ahm3texe commited on
Commit
f083c5c
·
verified ·
1 Parent(s): 279c3dd

Upload 28 files

Browse files
app.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import threading
4
+ import time
5
+ import numpy as np
6
+ import gradio as gr
7
+ import pygame
8
+
9
+ # 1. Headless Config (Must be before pygame.init)
10
+ os.environ["SDL_VIDEODRIVER"] = "dummy"
11
+ os.environ["SDL_AUDIODRIVER"] = "dummy"
12
+
13
+ # 2. Path Setup
14
+ current_dir = os.path.dirname(os.path.abspath(__file__))
15
+ pydino_path = os.path.join(current_dir, "pydino")
16
+ sys.path.append(pydino_path)
17
+
18
+ # 3. Imports from existing codebase
19
+ import pickle
20
+ from dimensions import Dimensions
21
+ from watch_model import ModelWatcher
22
+ # Import necessary classes effectively re-using watch_model logic
23
+ # We need to load the brain manually
24
+
25
+ # Global State
26
+ GAME_INSTANCE = None
27
+ CURRENT_FRAME = None
28
+ CURRENT_FRAME = None
29
+ CURRENT_BRAIN_DATA = {"inputs": [], "hidden": [], "outputs": []}
30
+ BRAIN_WEIGHTS_JSON = "{}"
31
+ LOCK = threading.Lock()
32
+ TARGET_TPS = 60
33
+
34
+ # --- JS Visualizer Code (Global Injection) ---
35
+ # This JS will be injected once at page load via demo.load(js=...)
36
+ # It defines window.drawBrain which is called by the data stream.
37
+
38
+ def load_web_ui_asset(filename):
39
+ asset_path = os.path.join(current_dir, "neurodino", "web-ui", filename)
40
+ if not os.path.exists(asset_path):
41
+ print(f"Warning: Asset not found: {asset_path}")
42
+ return ""
43
+ with open(asset_path, "r", encoding="utf-8") as f:
44
+ return f.read()
45
+
46
+ VISUALIZER_JS_TEMPLATE = load_web_ui_asset("visualizer.js")
47
+
48
+ def load_brain(brain_path="best_brain.pkl"):
49
+ """Load the best brain."""
50
+ if not os.path.exists(brain_path):
51
+ return None
52
+ try:
53
+ with open(brain_path, "rb") as f:
54
+ data = pickle.load(f)
55
+ if isinstance(data, tuple):
56
+ return data[0] # brain, score
57
+ return data # just brain
58
+ except Exception as e:
59
+ print(f"Error loading brain: {e}")
60
+ return None
61
+
62
+ def game_thread_func():
63
+ """Background thread calling the game update loop."""
64
+ global GAME_INSTANCE, CURRENT_FRAME, CURRENT_BRAIN_DATA, TARGET_TPS
65
+
66
+ clock = pygame.time.Clock()
67
+ print("Game thread started.")
68
+
69
+ while True:
70
+ try:
71
+ if GAME_INSTANCE:
72
+ pygame.event.pump()
73
+ GAME_INSTANCE.update()
74
+
75
+ if GAME_INSTANCE.crashed:
76
+ GAME_INSTANCE.restart_game()
77
+
78
+ view = pygame.surfarray.array3d(GAME_INSTANCE.screen)
79
+ view = view.transpose([1, 0, 2])
80
+
81
+ if hasattr(GAME_INSTANCE, 'brain'):
82
+ b = GAME_INSTANCE.brain
83
+ # Force flatten using numpy to handle both lists and arrays robustly
84
+ inputs = np.array(b.last_inputs).flatten().tolist()
85
+ hidden = np.array(b.last_hidden).flatten().tolist()
86
+ outputs = np.array(b.last_outputs).flatten().tolist()
87
+
88
+ brain_data = {
89
+ "inputs": inputs,
90
+ "hidden": hidden,
91
+ "outputs": outputs
92
+ }
93
+ else:
94
+ brain_data = {"inputs": [], "hidden": [], "outputs": []}
95
+
96
+ with LOCK:
97
+ CURRENT_FRAME = view
98
+ CURRENT_BRAIN_DATA = brain_data
99
+
100
+ if TARGET_TPS > 0:
101
+ clock.tick(TARGET_TPS)
102
+ else:
103
+ clock.tick()
104
+
105
+ except Exception as e:
106
+ print(f"Error in game loop: {e}")
107
+ time.sleep(1)
108
+
109
+ def set_speed(choice):
110
+ global TARGET_TPS
111
+ if choice == "Yavaş (30 FPS)":
112
+ TARGET_TPS = 30
113
+ elif choice == "Normal (60 FPS)":
114
+ TARGET_TPS = 60
115
+ elif choice == "Hızlı (120 FPS)":
116
+ TARGET_TPS = 120
117
+ elif choice == "Maksimum (Unlimited)":
118
+ TARGET_TPS = 0
119
+ return f"Hız ayarlandı: {choice}"
120
+
121
+ def start_game_server():
122
+ global GAME_INSTANCE, BRAIN_WEIGHTS_JSON
123
+
124
+ pygame.init()
125
+ brain = load_brain()
126
+ if not brain: return False
127
+
128
+ # Extract static weights
129
+ import json
130
+ weights = {
131
+ "ih": brain.weights_ih.tolist() if hasattr(brain.weights_ih, 'tolist') else brain.weights_ih,
132
+ "ho": brain.weights_ho.tolist() if hasattr(brain.weights_ho, 'tolist') else brain.weights_ho,
133
+ "bh": np.array(brain.bias_h).flatten().tolist(),
134
+ "bo": np.array(brain.bias_o).flatten().tolist()
135
+ }
136
+ BRAIN_WEIGHTS_JSON = json.dumps(weights)
137
+
138
+ pygame.display.set_mode((600, 150))
139
+ dims = Dimensions(width=600, height=150)
140
+ game_surface = pygame.Surface((dims.width, dims.height))
141
+
142
+ GAME_INSTANCE = ModelWatcher(game_surface, dims, brain, silent=False)
143
+ GAME_INSTANCE.start()
144
+
145
+ t = threading.Thread(target=game_thread_func, daemon=True)
146
+ t.start()
147
+ return True
148
+
149
+ def data_producer():
150
+ """Yields (image, json_data) tuple."""
151
+ while True:
152
+ with LOCK:
153
+ yield (CURRENT_FRAME, CURRENT_BRAIN_DATA)
154
+ # Reduce sleep to almost zero (1ms) to maximize frame rate
155
+ # Gradio will yield as fast as network permits
156
+
157
+ # Adaptive Streaming: If Unlimited (0), throttle stream to 20 FPS to save CPU
158
+ if TARGET_TPS == 0:
159
+ time.sleep(0.05) # 20 FPS cap for visuals
160
+ else:
161
+ time.sleep(1.0 / TARGET_TPS) # Sync with Game Speed (e.g. 60 FPS) for smoothness
162
+
163
+ # --- Gradio UI ---
164
+ description = """
165
+ # 🦖 NeuroDino Canlı Yayın
166
+ Bu demo, **Genetik Algoritma** ile eğitilmiş bir Yapay Zeka'nın (Neural Network) canlı oynayışını gösterir.
167
+ """
168
+
169
+ # HTML for the Canvas (No Script here)
170
+ CANVAS_HTML = load_web_ui_asset("canvas.html")
171
+
172
+ # CSS
173
+ CSS = load_web_ui_asset("style.css")
174
+
175
+ # HTML for Custom Controls
176
+ CUSTOM_CONTROLS_HTML = load_web_ui_asset("custom_controls.html")
177
+
178
+ # JS to force Light Mode
179
+ FORCE_LIGHT_JS = """
180
+ function refresh() {
181
+ const url = new URL(window.location);
182
+ if (url.searchParams.get('__theme') !== 'light') {
183
+ url.searchParams.set('__theme', 'light');
184
+ window.location.href = url.href;
185
+ }
186
+ }
187
+ """
188
+
189
+ with gr.Blocks(css=CSS, theme=gr.themes.Default(), js="document.body.classList.remove('dark');") as demo:
190
+
191
+ with gr.Row(elem_id="main_row"):
192
+ with gr.Column(scale=1.5, elem_id="left_game_col"):
193
+ # Added elem_id="game_display" and removed internal height cap
194
+ image_out = gr.Image(label="Oyun Görünümü", streaming=True, elem_id="game_display", show_label=False)
195
+
196
+ # Custom Controls (Pixel Art Style)
197
+ gr.HTML(CUSTOM_CONTROLS_HTML)
198
+
199
+ with gr.Row():
200
+ speed_radio = gr.Radio(
201
+ choices=["Yavaş (30 FPS)", "Normal (60 FPS)", "Hızlı (120 FPS)", "Maksimum (Unlimited)"],
202
+ value="Normal (60 FPS)",
203
+ label="Oyun Hızı",
204
+ interactive=True
205
+ )
206
+
207
+ with gr.Column(scale=2):
208
+ # Just the canvas container
209
+ gr.HTML(CANVAS_HTML)
210
+
211
+ # Hidden JSON sink
212
+ brain_data_sink = gr.JSON(visible=False)
213
+
214
+ # Initialize Server & Get JS
215
+ if start_game_server():
216
+ # Inject Javascript Global Code
217
+ js_code = VISUALIZER_JS_TEMPLATE.replace("__WEIGHTS_PLACEHOLDER__", BRAIN_WEIGHTS_JSON)
218
+ demo.load(None, None, None, js=js_code)
219
+
220
+
221
+
222
+
223
+ # Stream Loop
224
+ demo.load(data_producer, inputs=None, outputs=[image_out, brain_data_sink])
225
+
226
+ # Trigger JS draw on data update
227
+ brain_data_sink.change(
228
+ fn=None,
229
+ inputs=[brain_data_sink],
230
+ js="(data) => { if(window.drawBrain) window.drawBrain(data); }"
231
+ )
232
+
233
+ # Speed control connection
234
+ status_msg = gr.Markdown(visible=False)
235
+ speed_radio.change(fn=set_speed, inputs=speed_radio, outputs=status_msg)
236
+
237
+ if __name__ == "__main__":
238
+ # Server already started in block definition to get weights, just launch
239
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860, share=True)
best_brain.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5c129d232c3cfe7025f4efbec52209217c6177488090ffa36c3c7d4c7ddb947
3
+ size 7200
neurodino/web-ui/canvas.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <div id="canvas-container"
2
+ style="position: relative; width: 100%; min-height: 600px; height: auto; background: transparent; border-radius: 8px; overflow: auto;">
3
+ <canvas id="brainCanvas"></canvas>
4
+ </div>
neurodino/web-ui/custom_controls.html ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="neuro-custom-controls"
2
+ style="font-family: 'PressStart2P', monospace; background: #fff; padding: 20px 20px 5px 20px; border: 2px solid #535353; margin-top: 20px;">
3
+ <style>
4
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
5
+
6
+ /* Header Toggle Style */
7
+ .neuro-header {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ align-items: center;
11
+ cursor: pointer;
12
+ padding-bottom: 15px;
13
+ user-select: none;
14
+ }
15
+
16
+ #neuro-custom-controls h3 {
17
+ margin: 0;
18
+ color: #535353;
19
+ font-size: 14px;
20
+ }
21
+
22
+ #neuro-arrow {
23
+ transition: transform 0.3s ease;
24
+ transform: rotate(-90deg);
25
+ /* Start collapsed state */
26
+ font-size: 12px;
27
+ color: #535353;
28
+ }
29
+
30
+ /* Collapsible Content */
31
+ #neuro-controls-content {
32
+ max-height: 0;
33
+ overflow: hidden;
34
+ transition: max-height 0.4s ease-out, opacity 0.3s ease-out, padding 0.3s ease;
35
+ opacity: 0;
36
+ padding-top: 0;
37
+ padding-bottom: 0;
38
+ }
39
+
40
+ .neuro-control-group {
41
+ margin-bottom: 20px;
42
+ }
43
+
44
+ .neuro-row {
45
+ display: flex;
46
+ align-items: center;
47
+ margin-bottom: 10px;
48
+ gap: 10px;
49
+ }
50
+
51
+ .neuro-label {
52
+ font-size: 10px;
53
+ color: #535353;
54
+ min-width: 120px;
55
+ }
56
+
57
+ /* Custom Pixel Checkbox */
58
+ .neuro-checkbox-wrapper {
59
+ display: flex;
60
+ align-items: center;
61
+ cursor: pointer;
62
+ user-select: none;
63
+ }
64
+
65
+ .neuro-checkbox {
66
+ width: 16px;
67
+ height: 16px;
68
+ border: 2px solid #535353;
69
+ display: inline-block;
70
+ margin-right: 8px;
71
+ position: relative;
72
+ }
73
+
74
+ .neuro-checkbox.checked::after {
75
+ content: '';
76
+ position: absolute;
77
+ top: 2px;
78
+ left: 2px;
79
+ width: 8px;
80
+ height: 8px;
81
+ background-color: #535353;
82
+ }
83
+
84
+ /* Custom Pixel Slider */
85
+ .neuro-slider-container {
86
+ flex-grow: 1;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 10px;
90
+ padding: 8px 0;
91
+ /* Add vertical padding for thumb overflow */
92
+ overflow: visible;
93
+ }
94
+
95
+ .neuro-slider {
96
+ -webkit-appearance: none;
97
+ appearance: none;
98
+ width: 100%;
99
+ height: 4px;
100
+ background: #e0e0e0;
101
+ outline: none;
102
+ border: 1px solid #535353;
103
+ }
104
+
105
+ .neuro-slider::-webkit-slider-thumb {
106
+ -webkit-appearance: none;
107
+ appearance: none;
108
+ width: 16px;
109
+ height: 16px;
110
+ background: #fff;
111
+ border: 2px solid #535353;
112
+ box-sizing: border-box;
113
+ margin-top: 0px;
114
+ /* Center thumb on track */
115
+ cursor: pointer;
116
+ }
117
+
118
+ .neuro-slider::-webkit-slider-thumb:hover {
119
+ background: #535353;
120
+ }
121
+
122
+ .neuro-value {
123
+ font-size: 10px;
124
+ min-width: 30px;
125
+ text-align: right;
126
+ }
127
+
128
+ /* Color Picker Wrapper */
129
+ .neuro-color {
130
+ width: 24px;
131
+ height: 24px;
132
+ border: 2px solid #535353;
133
+ padding: 0;
134
+ background: none;
135
+ cursor: pointer;
136
+ -webkit-appearance: none;
137
+ appearance: none;
138
+ }
139
+
140
+ .neuro-color::-webkit-color-swatch-wrapper {
141
+ padding: 0;
142
+ }
143
+
144
+ .neuro-color::-webkit-color-swatch {
145
+ border: none;
146
+ }
147
+ </style>
148
+
149
+ <div class="neuro-header" onclick="toggleCustomControls()">
150
+ <h3>CUSTOM KONTROL PANELI</h3>
151
+ <span id="neuro-arrow">▼</span>
152
+ </div>
153
+
154
+ <!-- Collapsible Wrapper -->
155
+ <div id="neuro-controls-content">
156
+ <div style="border-top: 2px solid #535353; margin-bottom: 20px;"></div>
157
+
158
+ <!-- GROUP 1: EDGES -->
159
+ <div class="neuro-control-group">
160
+ <div class="neuro-header" onclick="toggleNeuroGroup('group-edges', 'arrow-edges')"
161
+ style="padding-bottom: 5px;">
162
+ <h4 style="margin: 0; color: #535353; font-size: 10px; text-decoration: none;">BAĞLANTILAR (EDGES)</h4>
163
+ <span id="arrow-edges"
164
+ style="font-size: 10px; color: #535353; transition: transform 0.3s ease; transform: rotate(-90deg);">▼</span>
165
+ </div>
166
+
167
+ <div id="group-edges"
168
+ style="max-height: 0; overflow: hidden; transition: max-height 0.3s ease, opacity 0.3s ease; opacity: 0; padding-left: 5px;">
169
+ <div class="neuro-row neuro-checkbox-wrapper" onclick="toggleNeuroCheck('bezier_check', 'useBezier')">
170
+ <div id="neuro_bezier_check" class="neuro-checkbox"></div>
171
+ <span class="neuro-label">Bezier (Kavisli)</span>
172
+ </div>
173
+ <div class="neuro-row neuro-checkbox-wrapper"
174
+ onclick="toggleNeuroCheck('vertical_check', 'useVerticalLayout')">
175
+ <div id="neuro_vertical_check" class="neuro-checkbox"></div>
176
+ <span class="neuro-label">Dikey Görünüm (Vertical)</span>
177
+ </div>
178
+ <div class="neuro-row neuro-checkbox-wrapper"
179
+ onclick="toggleNeuroCheck('active_check', 'showActiveEdgesOnly')">
180
+ <div id="neuro_active_check" class="neuro-checkbox"></div>
181
+ <span class="neuro-label">Canlı Sinyal Modu</span>
182
+ </div>
183
+
184
+ <div class="neuro-row neuro-checkbox-wrapper"
185
+ onclick="toggleNeuroCheck('weight_color_check', 'useWeightColor')">
186
+ <div id="neuro_weight_color_check" class="neuro-checkbox"></div>
187
+ <span class="neuro-label">Ağırlığa Göre Renk</span>
188
+ </div>
189
+
190
+ <!-- Conditional Color Pickers -->
191
+ <div id="neuro-weight-colors" style="display: none; padding-left: 20px; margin-bottom: 10px;">
192
+ <div class="neuro-row">
193
+ <span class="neuro-label" style="min-width: 100px;">Pozitif (+):</span>
194
+ <input type="color" class="neuro-color" value="#0000ff"
195
+ oninput="updateNeuroColor('posColor', this.value)">
196
+ </div>
197
+ <div class="neuro-row">
198
+ <span class="neuro-label" style="min-width: 100px;">Negatif (-):</span>
199
+ <input type="color" class="neuro-color" value="#ff0000"
200
+ oninput="updateNeuroColor('negColor', this.value)">
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Default Color Picker (Visible when Weight Color is OFF) -->
205
+ <div id="neuro-default-color" style="display: block; padding-left: 20px; margin-bottom: 10px;">
206
+ <div class="neuro-row">
207
+ <span class="neuro-label" style="min-width: 100px;">Standart Renk:</span>
208
+ <input type="color" class="neuro-color" value="#505050"
209
+ oninput="updateNeuroColor('defaultEdgeColor', this.value)">
210
+ </div>
211
+ </div>
212
+
213
+ <div class="neuro-row neuro-checkbox-wrapper"
214
+ onclick="toggleNeuroCheck('prop_width_check', 'useProportionalWidth')">
215
+ <div id="neuro_prop_width_check" class="neuro-checkbox"></div>
216
+ <span class="neuro-label">Ağırlığa Göre Kalınlık</span>
217
+ </div>
218
+
219
+ <div class="neuro-row">
220
+ <span class="neuro-label">Baz Kalınlık</span>
221
+ <div class="neuro-slider-container">
222
+ <input type="range" class="neuro-slider" min="0" max="3" step="0.1" value="0.5"
223
+ oninput="updateNeuroVal('edgeWidth', this.value, 'val_edgeWidth')">
224
+ <span id="val_edgeWidth" class="neuro-value">0.5</span>
225
+ </div>
226
+ </div>
227
+
228
+ <div class="neuro-row neuro-checkbox-wrapper"
229
+ onclick="toggleNeuroCheck('prop_opacity_check', 'useProportionalOpacity')">
230
+ <div id="neuro_prop_opacity_check" class="neuro-checkbox"></div>
231
+ <span class="neuro-label">Ağırlığa Göre Opaklık</span>
232
+ </div>
233
+
234
+ <div class="neuro-row">
235
+ <span class="neuro-label">Baz Opaklık</span>
236
+ <div class="neuro-slider-container">
237
+ <input type="range" class="neuro-slider" min="0.1" max="1.0" step="0.1" value="1.0"
238
+ oninput="updateNeuroVal('edgeOpacity', this.value, 'val_opacity')">
239
+ <span id="val_opacity" class="neuro-value">1.0</span>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <div style="border-top: 1px dashed #ccc; margin: 15px 0;"></div>
246
+
247
+ <!-- GROUP 2: NODES -->
248
+ <div class="neuro-control-group">
249
+ <div class="neuro-header" onclick="toggleNeuroGroup('group-nodes', 'arrow-nodes')"
250
+ style="padding-bottom: 5px;">
251
+ <h4 style="margin: 0; color: #535353; font-size: 10px; text-decoration: none;">NÖRONLAR (NODES)</h4>
252
+ <span id="arrow-nodes"
253
+ style="font-size: 10px; color: #535353; transition: transform 0.3s ease; transform: rotate(-90deg);">▼</span>
254
+ </div>
255
+
256
+ <div id="group-nodes"
257
+ style="max-height: 0; overflow: hidden; transition: max-height 0.3s ease, opacity 0.3s ease; opacity: 0; padding-left: 5px;">
258
+ <div class="neuro-row neuro-checkbox-wrapper"
259
+ onclick="toggleNeuroCheck('pixel_check', 'usePixelNodes')">
260
+ <div id="neuro_pixel_check" class="neuro-checkbox"></div>
261
+ <span class="neuro-label">Piksel Modu</span>
262
+ </div>
263
+
264
+ <div class="neuro-row neuro-checkbox-wrapper" onclick="toggleNeuroCheck('bias_check', 'showBiases')">
265
+ <div id="neuro_bias_check" class="neuro-checkbox checked"></div> <!-- Default Checked -->
266
+ <span class="neuro-label">Bias Göster</span>
267
+ </div>
268
+
269
+ <div class="neuro-row neuro-checkbox-wrapper"
270
+ onclick="toggleNeuroCheck('activation_color_check', 'useNodeActivationColor')">
271
+ <div id="neuro_activation_color_check" class="neuro-checkbox"></div>
272
+ <span class="neuro-label">Aktivasyon Rengi</span>
273
+ </div>
274
+
275
+ <!-- Conditional Activation Colors -->
276
+ <div id="neuro-activation-colors" style="display: none; padding-left: 20px; margin-bottom: 10px;">
277
+ <div class="neuro-row">
278
+ <span class="neuro-label" style="min-width: 100px;">Pozitif (+):</span>
279
+ <input type="color" class="neuro-color" value="#0000ff"
280
+ oninput="updateNeuroColor('posColor', this.value)">
281
+ </div>
282
+ <div class="neuro-row">
283
+ <span class="neuro-label" style="min-width: 100px;">Negatif (-):</span>
284
+ <input type="color" class="neuro-color" value="#ff0000"
285
+ oninput="updateNeuroColor('negColor', this.value)">
286
+ </div>
287
+ </div>
288
+
289
+ <div class="neuro-row">
290
+ <span class="neuro-label">Node Rengi:</span>
291
+ <input type="color" class="neuro-color" value="#ffffff"
292
+ oninput="updateNeuroColor('nodeColor', this.value)">
293
+ </div>
294
+
295
+ <div class="neuro-row">
296
+ <span class="neuro-label">Çerçeve:</span>
297
+ <input type="color" class="neuro-color" value="#333333"
298
+ oninput="updateNeuroColor('nodeBorderColor', this.value)">
299
+ </div>
300
+
301
+ <div class="neuro-row">
302
+ <span class="neuro-label">Node Çapı</span>
303
+ <div class="neuro-slider-container">
304
+ <input type="range" class="neuro-slider" min="2" max="50" step="1" value="10"
305
+ oninput="updateNeuroVal('targetRadius', this.value/2, 'val_radius', this.value)">
306
+ <span id="val_radius" class="neuro-value">10</span>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <div style="border-top: 1px dashed #ccc; margin: 15px 0;"></div>
313
+
314
+ <!-- GROUP 3: LAYOUT -->
315
+ <div class="neuro-control-group">
316
+ <div class="neuro-header" onclick="toggleNeuroGroup('group-layout', 'arrow-layout')"
317
+ style="padding-bottom: 5px;">
318
+ <h4 style="margin: 0; color: #535353; font-size: 10px; text-decoration: none;">YERLEŞİM (LAYOUT)</h4>
319
+ <span id="arrow-layout"
320
+ style="font-size: 10px; color: #535353; transition: transform 0.3s ease; transform: rotate(-90deg);">▼</span>
321
+ </div>
322
+
323
+ <div id="group-layout"
324
+ style="max-height: 0; overflow: hidden; transition: max-height 0.3s ease, opacity 0.3s ease; opacity: 0; padding-left: 5px;">
325
+ <div class="neuro-row">
326
+ <span class="neuro-label">Katman Aralığı</span>
327
+ <div class="neuro-slider-container">
328
+ <input type="range" class="neuro-slider" min="50" max="400" step="10" value="290"
329
+ oninput="updateNeuroVal('layerSpacing', this.value, 'val_spacing')">
330
+ <span id="val_spacing" class="neuro-value">290</span>
331
+ </div>
332
+ </div>
333
+
334
+ <div class="neuro-row">
335
+ <span class="neuro-label">Giriş Dikey</span>
336
+ <div class="neuro-slider-container">
337
+ <input type="range" class="neuro-slider" min="5" max="100" step="1" value="30"
338
+ oninput="updateNeuroVal('vSpacingInput', this.value, 'val_v_input')">
339
+ <span id="val_v_input" class="neuro-value">30</span>
340
+ </div>
341
+ </div>
342
+
343
+ <div class="neuro-row">
344
+ <span class="neuro-label">Gizli Dikey</span>
345
+ <div class="neuro-slider-container">
346
+ <input type="range" class="neuro-slider" min="5" max="100" step="1" value="10"
347
+ oninput="updateNeuroVal('vSpacingHidden', this.value, 'val_v_hidden')">
348
+ <span id="val_v_hidden" class="neuro-value">10</span>
349
+ </div>
350
+ </div>
351
+
352
+ <div class="neuro-row">
353
+ <span class="neuro-label">Çıkış Dikey</span>
354
+ <div class="neuro-slider-container">
355
+ <input type="range" class="neuro-slider" min="5" max="100" step="1" value="30"
356
+ oninput="updateNeuroVal('vSpacingOutput', this.value, 'val_v_output')">
357
+ <span id="val_v_output" class="neuro-value">30</span>
358
+ </div>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ </div>
neurodino/web-ui/style.css ADDED
The diff for this file is too large to render. See raw diff
 
neurodino/web-ui/visualizer.js ADDED
@@ -0,0 +1,1036 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function setupNeuroDino() {
2
+ // --- Color Lerp Helper ---
3
+ function lerpColor(colorA, colorB, t, alpha = 1.0) {
4
+ // Parse rgb(r, g, b) or hex format
5
+ const parseColor = (c) => {
6
+ if (c.startsWith('rgb')) {
7
+ const m = c.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
8
+ return m ? [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])] : [80, 80, 80];
9
+ } else if (c.startsWith('#')) {
10
+ const hex = c.slice(1);
11
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
12
+ }
13
+ return [80, 80, 80];
14
+ };
15
+ const a = parseColor(colorA);
16
+ const b = parseColor(colorB);
17
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
18
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
19
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
20
+ return `rgba(${r}, ${g}, ${bl}, ${alpha})`;
21
+ }
22
+
23
+ // Inject weights from Python
24
+ const weights = __WEIGHTS_PLACEHOLDER__;
25
+ window.useBezier = false;
26
+ window.edgeWidth = 0.5;
27
+ window.useProportionalWidth = false;
28
+ window.useProportionalOpacity = false;
29
+ window.useWeightColor = false;
30
+ window.usePixelNodes = false;
31
+ window.useVerticalLayout = false;
32
+ window.useNodeActivationColor = false;
33
+ window.showBiases = true;
34
+ window.posColor = "rgb(0, 0, 255)";
35
+ window.negColor = "rgb(255, 0, 0)";
36
+ window.defaultEdgeColor = "rgb(80, 80, 80)";
37
+ window.nodeColor = "white";
38
+ window.nodeBorderColor = "#333333";
39
+ window.edgeOpacity = 1.0;
40
+ window.showActiveEdgesOnly = false;
41
+
42
+ // --- UI Control Functions (Global) ---
43
+ window.updateNeuroVal = function (globalVar, value, displayId, displayVal) {
44
+ window[globalVar] = parseFloat(value);
45
+ if (displayId) {
46
+ const el = document.getElementById(displayId);
47
+ if (el) el.innerText = displayVal || value;
48
+ }
49
+ };
50
+
51
+ window.toggleNeuroCheck = function (elemId, globalVar) {
52
+ const el = document.getElementById('neuro_' + elemId);
53
+ if (!el) return;
54
+
55
+ const isChecked = el.classList.contains('checked');
56
+ if (isChecked) {
57
+ el.classList.remove('checked');
58
+ window[globalVar] = false;
59
+ } else {
60
+ el.classList.add('checked');
61
+ window[globalVar] = true;
62
+ }
63
+
64
+ // Special handling for Weight Color toggle to show/hide pickers
65
+ if (elemId === 'weight_color_check') {
66
+ const weightContainer = document.getElementById('neuro-weight-colors');
67
+ const defaultContainer = document.getElementById('neuro-default-color');
68
+
69
+ if (window[globalVar]) {
70
+ // Weight Color ON: Show Pos/Neg, Hide Default
71
+ if (weightContainer) weightContainer.style.display = 'block';
72
+ if (defaultContainer) defaultContainer.style.display = 'none';
73
+ } else {
74
+ // Weight Color OFF: Hide Pos/Neg, Show Default
75
+ if (weightContainer) weightContainer.style.display = 'none';
76
+ if (defaultContainer) defaultContainer.style.display = 'block';
77
+ }
78
+ }
79
+
80
+
81
+
82
+ // Special handling for Node Activation Color toggle
83
+ if (elemId === 'activation_color_check') {
84
+ const actContainer = document.getElementById('neuro-activation-colors');
85
+ if (actContainer) {
86
+ actContainer.style.display = window[globalVar] ? 'block' : 'none';
87
+ }
88
+
89
+ // Also update the inner group accordion height
90
+ const groupNodes = document.getElementById('group-nodes');
91
+ if (groupNodes && groupNodes.style.maxHeight) {
92
+ setTimeout(() => {
93
+ groupNodes.style.maxHeight = (groupNodes.scrollHeight + 20) + "px";
94
+ }, 10);
95
+ }
96
+ }
97
+
98
+ // Recalculate Accordion Height if it's open
99
+ const content = document.getElementById('neuro-controls-content');
100
+ if (content && content.style.maxHeight) {
101
+ // Wait for display change to render, then update height
102
+ setTimeout(() => {
103
+ content.style.maxHeight = (content.scrollHeight + 50) + "px";
104
+ }, 10);
105
+ }
106
+ };
107
+
108
+ window.updateNeuroColor = function (globalVar, value) {
109
+ window[globalVar] = value;
110
+ };
111
+
112
+
113
+ window.toggleCustomControls = function () {
114
+ const content = document.getElementById('neuro-controls-content');
115
+ const arrow = document.getElementById('neuro-arrow');
116
+
117
+ if (content.style.maxHeight) {
118
+ content.style.maxHeight = null;
119
+ content.style.paddingTop = "0px";
120
+ content.style.paddingBottom = "0px";
121
+ content.style.opacity = "0";
122
+ arrow.style.transform = "rotate(-90deg)";
123
+ } else {
124
+ // Add extra height for the padding we are about to add (10px top + 10px bottom = 20px)
125
+ content.style.maxHeight = (content.scrollHeight + 50) + "px";
126
+ content.style.paddingTop = "10px";
127
+ content.style.paddingBottom = "10px"; // match css
128
+ content.style.opacity = "1";
129
+ arrow.style.transform = "rotate(0deg)";
130
+ }
131
+ };
132
+
133
+ window.toggleNeuroGroup = function (groupId, arrowId) {
134
+ const content = document.getElementById(groupId);
135
+ const arrow = document.getElementById(arrowId);
136
+ const parentContent = document.getElementById('neuro-controls-content');
137
+
138
+ if (content.style.maxHeight && content.style.maxHeight !== "0px") {
139
+ // Collapse
140
+ content.style.maxHeight = "0px";
141
+ content.style.opacity = "0";
142
+ content.style.marginBottom = "0px";
143
+ if (arrow) arrow.style.transform = "rotate(-90deg)";
144
+ } else {
145
+ // Expand
146
+ content.style.maxHeight = (content.scrollHeight + 20) + "px";
147
+ content.style.opacity = "1";
148
+ content.style.marginBottom = "20px";
149
+ if (arrow) arrow.style.transform = "rotate(0deg)";
150
+ }
151
+
152
+ // Parent Height Recalculation
153
+ if (parentContent && parentContent.style.maxHeight) {
154
+ setTimeout(() => {
155
+ // Determine new height needed.
156
+ // We add a large buffer because 'scrollHeight' might not update instantly during transition,
157
+ // and max-height doesn't force height, just limits it.
158
+ // So setting it generously avoids cutting off content.
159
+ parentContent.style.maxHeight = (parentContent.scrollHeight + content.scrollHeight + 200) + "px";
160
+ }, 50);
161
+ }
162
+ };
163
+ // -------------------------------------
164
+
165
+ // Smooth Transition States
166
+ window.smoothEdgeWidth = 2.0;
167
+ window.smoothRadius = 10.0;
168
+ window.smoothEdgeOpacity = 1.0;
169
+ window.smoothLayerSpacing = 290;
170
+
171
+ window.smoothLayerSpacing = 290;
172
+
173
+ // Target Vertical Spacings (Controlled by UI)
174
+ window.vSpacingInput = 30;
175
+ window.vSpacingHidden = 10;
176
+ window.vSpacingOutput = 30;
177
+
178
+ // Smooth Variables (Initialized to target)
179
+ window.smoothVSpacingInput = 30;
180
+ window.smoothVSpacingHidden = 10;
181
+ window.smoothVSpacingOutput = 30;
182
+ window.smoothVSpacingOutput = 40;
183
+
184
+ window.smoothPropFactor = 0.0;
185
+ window.smoothPropOpacityFactor = 0.0;
186
+ window.smoothBezierFactor = 0.0;
187
+ window.smoothNodeColorFactor = 0.0;
188
+ window.smoothActiveFactor = 0.0;
189
+ window.smoothWeightColorFactor = 0.0;
190
+
191
+ window.layerSpacing = 290;
192
+ window.targetRadius = 5;
193
+ window.latestData = null;
194
+ window.vSpacingInput = 30;
195
+ window.vSpacingHidden = 10;
196
+ window.vSpacingHidden = 10;
197
+ window.vSpacingOutput = 40;
198
+
199
+ // Zoom & Pan State
200
+ window.vizScale = 1;
201
+ window.panX = 0;
202
+ window.panY = 0;
203
+ window.isDragging = false;
204
+ window.lastX = 0;
205
+ window.lastY = 0;
206
+
207
+ // Interaction State
208
+ window.activeNode = null; // {layer: 'input'|'hidden'|'output', index: 0}
209
+ window.nodePositions = []; // List of {x, y, r, layer, index} populated every frame
210
+
211
+ // Main Draw Function (Updates Data ONLY)
212
+ window.drawBrain = function (data) {
213
+ window.latestData = data;
214
+
215
+ // Start Loop if not started (idempotent check ideally,
216
+ // but we can just rely on the separate loop starter)
217
+ };
218
+
219
+ // Animation Loop
220
+ function renderLoop() {
221
+ requestAnimationFrame(renderLoop);
222
+
223
+ // Smooth Parameter Interpolation
224
+ if (window.smoothEdgeWidth === undefined) window.smoothEdgeWidth = window.edgeWidth;
225
+ // Lerp factor 0.1 for smooth transition
226
+ window.smoothEdgeWidth += (window.edgeWidth - window.smoothEdgeWidth) * 0.1;
227
+
228
+ // Clamp to avoid tiny jitter when close
229
+ if (Math.abs(window.edgeWidth - window.smoothEdgeWidth) < 0.001) {
230
+ window.smoothEdgeWidth = window.edgeWidth;
231
+ }
232
+
233
+ // Smooth Radius Interpolation
234
+ if (window.smoothRadius === undefined) window.smoothRadius = window.targetRadius;
235
+ window.smoothRadius += (window.targetRadius - window.smoothRadius) * 0.1;
236
+ if (Math.abs(window.targetRadius - window.smoothRadius) < 0.01) window.smoothRadius = window.targetRadius;
237
+
238
+ // Smooth Opacity Interpolation
239
+ if (window.smoothEdgeOpacity === undefined) window.smoothEdgeOpacity = window.edgeOpacity;
240
+ window.smoothEdgeOpacity += (window.edgeOpacity - window.smoothEdgeOpacity) * 0.1;
241
+ if (Math.abs(window.edgeOpacity - window.smoothEdgeOpacity) < 0.001) window.smoothEdgeOpacity = window.edgeOpacity;
242
+
243
+ // Smooth Layer Spacing Interpolation
244
+ if (window.smoothLayerSpacing === undefined) window.smoothLayerSpacing = window.layerSpacing;
245
+ window.smoothLayerSpacing += (window.layerSpacing - window.smoothLayerSpacing) * 0.1;
246
+ if (Math.abs(window.layerSpacing - window.smoothLayerSpacing) < 0.1) window.smoothLayerSpacing = window.layerSpacing;
247
+
248
+ // Smooth Vertical Spacing Interpolation
249
+ if (window.smoothVSpacingInput === undefined) window.smoothVSpacingInput = window.vSpacingInput;
250
+ window.smoothVSpacingInput += (window.vSpacingInput - window.smoothVSpacingInput) * 0.1;
251
+ if (Math.abs(window.vSpacingInput - window.smoothVSpacingInput) < 0.1) window.smoothVSpacingInput = window.vSpacingInput;
252
+
253
+ if (window.smoothVSpacingHidden === undefined) window.smoothVSpacingHidden = window.vSpacingHidden;
254
+ window.smoothVSpacingHidden += (window.vSpacingHidden - window.smoothVSpacingHidden) * 0.1;
255
+ if (Math.abs(window.vSpacingHidden - window.smoothVSpacingHidden) < 0.1) window.smoothVSpacingHidden = window.vSpacingHidden;
256
+
257
+ if (window.smoothVSpacingOutput === undefined) window.smoothVSpacingOutput = window.vSpacingOutput;
258
+ window.smoothVSpacingOutput += (window.vSpacingOutput - window.smoothVSpacingOutput) * 0.1;
259
+ if (Math.abs(window.vSpacingOutput - window.smoothVSpacingOutput) < 0.1) window.smoothVSpacingOutput = window.vSpacingOutput;
260
+
261
+ // Smooth Proportional Width Factor (Toggle Animation)
262
+ const targetPropFactor = window.useProportionalWidth ? 1.0 : 0.0;
263
+ if (window.smoothPropFactor === undefined) window.smoothPropFactor = targetPropFactor;
264
+ window.smoothPropFactor += (targetPropFactor - window.smoothPropFactor) * 0.1;
265
+
266
+ // Smooth Proportional Opacity Factor
267
+ const targetPropOpacityFactor = window.useProportionalOpacity ? 1.0 : 0.0;
268
+ if (window.smoothPropOpacityFactor === undefined) window.smoothPropOpacityFactor = targetPropOpacityFactor;
269
+ window.smoothPropOpacityFactor += (targetPropOpacityFactor - window.smoothPropOpacityFactor) * 0.1;
270
+
271
+ // Smooth Bezier Factor (0.0 = Straight, 1.0 = Curved)
272
+ const targetBezier = window.useBezier ? 1.0 : 0.0;
273
+ if (window.smoothBezierFactor === undefined) window.smoothBezierFactor = targetBezier;
274
+ window.smoothBezierFactor += (targetBezier - window.smoothBezierFactor) * 0.1;
275
+
276
+ // Smooth Node Color Factor
277
+ const targetNodeColor = window.useNodeActivationColor ? 1.0 : 0.0;
278
+ if (window.smoothNodeColorFactor === undefined) window.smoothNodeColorFactor = targetNodeColor;
279
+ window.smoothNodeColorFactor += (targetNodeColor - window.smoothNodeColorFactor) * 0.1;
280
+
281
+ let targetActiveFactor = window.showActiveEdgesOnly ? 1.0 : 0.0;
282
+ window.smoothActiveFactor += (targetActiveFactor - window.smoothActiveFactor) * 0.1;
283
+
284
+ // Smooth Weight Color Factor
285
+ const targetWeightColorFactor = window.useWeightColor ? 1.0 : 0.0;
286
+ if (window.smoothWeightColorFactor === undefined) window.smoothWeightColorFactor = targetWeightColorFactor;
287
+ window.smoothWeightColorFactor += (targetWeightColorFactor - window.smoothWeightColorFactor) * 0.1;
288
+
289
+ // Smooth Vertical Layout Factor
290
+ const targetVerticalFactor = window.useVerticalLayout ? 1.0 : 0.0;
291
+ if (window.smoothVerticalFactor === undefined) window.smoothVerticalFactor = targetVerticalFactor;
292
+ window.smoothVerticalFactor += (targetVerticalFactor - window.smoothVerticalFactor) * 0.1;
293
+
294
+ // --- Data Interpolation (Signal Smoothing) ---
295
+ if (!window.displayData && window.latestData) {
296
+ // First frame initialization (Deep Clone to avoid ref issues)
297
+ window.displayData = JSON.parse(JSON.stringify(window.latestData));
298
+ }
299
+
300
+ if (window.displayData && window.latestData) {
301
+ const lerpFactor = 0.15; // Tuning: 0.15 gives a nice fluid feel
302
+
303
+ // Helper to Lerp Value or Object->Value
304
+ const lerpVal = (current, target) => {
305
+ const cVal = (typeof current === 'object' && current !== null) ? current.val : current;
306
+ const tVal = (typeof target === 'object' && target !== null) ? target.val : target;
307
+
308
+ // If structure mismatch or undefined, jump to target
309
+ if (cVal === undefined || tVal === undefined) return target;
310
+
311
+ const newVal = cVal + (tVal - cVal) * lerpFactor;
312
+
313
+ if (typeof current === 'object' && current !== null) {
314
+ current.val = newVal;
315
+ return current;
316
+ }
317
+ return newVal;
318
+ };
319
+
320
+ // Interpolate Inputs
321
+ if (window.displayData.inputs && window.latestData.inputs) {
322
+ for (let i = 0; i < window.latestData.inputs.length; i++) {
323
+ // Ensure source array is large enough
324
+ if (window.displayData.inputs[i] === undefined) window.displayData.inputs[i] = window.latestData.inputs[i];
325
+ else window.displayData.inputs[i] = lerpVal(window.displayData.inputs[i], window.latestData.inputs[i]);
326
+ }
327
+ }
328
+
329
+ // Interpolate Hidden
330
+ if (window.displayData.hidden && window.latestData.hidden) {
331
+ for (let i = 0; i < window.latestData.hidden.length; i++) {
332
+ if (window.displayData.hidden[i] === undefined) window.displayData.hidden[i] = window.latestData.hidden[i];
333
+ else window.displayData.hidden[i] = lerpVal(window.displayData.hidden[i], window.latestData.hidden[i]);
334
+ }
335
+ }
336
+
337
+ // Interpolate Outputs
338
+ if (window.displayData.outputs && window.latestData.outputs) {
339
+ for (let i = 0; i < window.latestData.outputs.length; i++) {
340
+ if (window.displayData.outputs[i] === undefined) window.displayData.outputs[i] = window.latestData.outputs[i];
341
+ else window.displayData.outputs[i] = lerpVal(window.displayData.outputs[i], window.latestData.outputs[i]);
342
+ }
343
+ }
344
+ }
345
+
346
+ const canvas = document.getElementById('brainCanvas');
347
+ if (!canvas) return;
348
+
349
+ // Initialize Events Once
350
+ if (!canvas.listenersAdded) {
351
+ canvas.listenersAdded = true;
352
+
353
+ canvas.addEventListener('mousedown', (e) => {
354
+ const rect = canvas.getBoundingClientRect();
355
+ const mouseX = e.clientX - rect.left;
356
+ const mouseY = e.clientY - rect.top;
357
+
358
+ // Get World Coordinates
359
+ const dpr = window.devicePixelRatio || 1;
360
+ // Note: nodePositions are stored in LOGICAL coordinates (unscaled by dpr, but scaled by vizScale in world?)
361
+ // Actually my nodePositions capture logic below uses the transform?
362
+ // Let's ensure nodePositions stores WORLD coordinates (before Pan/Scale).
363
+ // Wait, if I grab x,y inside drawLayer, that is in World Space (after translate/scale).
364
+ // No, ctx calls use Local Coords.
365
+ // ctx.translate(panX, panY); ctx.scale(scale); ctx.arc(x,y).
366
+ // So 'x, y' in drawLayer are LOCAL coordinates relative to the scene origin (0,0).
367
+ // So we need to transform Mouse -> View -> World.
368
+
369
+ const worldX = (mouseX - window.panX) / window.vizScale;
370
+ const worldY = (mouseY - window.panY) / window.vizScale;
371
+
372
+ // Check collisions
373
+ let hitNode = null;
374
+ for (const node of window.nodePositions) {
375
+ const dx = worldX - node.x;
376
+ const dy = worldY - node.y;
377
+ if (dx * dx + dy * dy < node.r * node.r) {
378
+ hitNode = node;
379
+ break;
380
+ }
381
+ }
382
+
383
+ if (hitNode) {
384
+ window.activeNode = { layer: hitNode.layer, index: hitNode.index };
385
+ window.isDragging = false; // Don't pan if we clicked a node
386
+ } else {
387
+ window.activeNode = null;
388
+ window.isDragging = true;
389
+ window.lastX = e.clientX;
390
+ window.lastY = e.clientY;
391
+ canvas.style.cursor = 'grabbing';
392
+ }
393
+ });
394
+
395
+ window.addEventListener('mouseup', () => {
396
+ if (window.activeNode) {
397
+ window.activeNode = null; // Release hold
398
+ }
399
+ window.isDragging = false;
400
+ if (canvas) canvas.style.cursor = 'grab';
401
+ });
402
+
403
+ canvas.addEventListener('mouseleave', () => {
404
+ window.isDragging = false;
405
+ if (canvas) canvas.style.cursor = 'grab';
406
+ });
407
+
408
+ canvas.addEventListener('mousemove', (e) => {
409
+ if (!window.isDragging) return;
410
+ const dx = e.clientX - window.lastX;
411
+ const dy = e.clientY - window.lastY;
412
+ window.panX += dx;
413
+ window.panY += dy;
414
+ window.lastX = e.clientX;
415
+ window.lastY = e.clientY;
416
+ });
417
+
418
+ canvas.addEventListener('wheel', (e) => {
419
+ e.preventDefault();
420
+ const zoomIntensity = 0.0015;
421
+ const rect = canvas.getBoundingClientRect();
422
+ const mouseX = e.clientX - rect.left;
423
+ const mouseY = e.clientY - rect.top;
424
+
425
+ const worldX = (mouseX - window.panX) / window.vizScale;
426
+ const worldY = (mouseY - window.panY) / window.vizScale;
427
+
428
+ const factor = Math.exp(-e.deltaY * zoomIntensity);
429
+ // Unlimited Zoom (0.001x to 1000x)
430
+ const newScale = Math.min(Math.max(0.001, window.vizScale * factor), 1000);
431
+
432
+ window.vizScale = newScale;
433
+ window.panX = mouseX - worldX * newScale;
434
+ window.panY = mouseY - worldY * newScale;
435
+ });
436
+
437
+ canvas.style.cursor = 'grab';
438
+ }
439
+
440
+ const data = window.displayData || window.latestData;
441
+ if (!data || !weights) return;
442
+
443
+ const ctx = canvas.getContext('2d');
444
+
445
+ // Resize & Auto-Expand
446
+ const pad = 40;
447
+ const iCount = data.inputs.length;
448
+ const hCount = data.hidden.length;
449
+ const oCount = data.outputs.length;
450
+
451
+ // Auto-Scale Spacing to prevent Overlap
452
+ // Ensure spacing is at least (Diameter + 2px padding)
453
+ const minSpacing = (window.smoothRadius * 2) + 2;
454
+
455
+ // Use effective spacing (User setting OR Minimum required)
456
+ const effSpacingInput = Math.max(window.smoothVSpacingInput, minSpacing);
457
+ const effSpacingHidden = Math.max(window.smoothVSpacingHidden, minSpacing);
458
+ const effSpacingOutput = Math.max(window.smoothVSpacingOutput, minSpacing);
459
+
460
+ // Calculate needed height
461
+ const hInput = (iCount - 1) * effSpacingInput;
462
+ const hHidden = (hCount - 1) * effSpacingHidden;
463
+ const hOutput = (oCount - 1) * effSpacingOutput;
464
+
465
+ const contentHeight = Math.max(hInput, hHidden, hOutput);
466
+ const neededHeight = contentHeight + (pad * 2);
467
+
468
+ if (canvas.parentElement) {
469
+ let clientW, clientH;
470
+
471
+ // Windowed Mode: Auto-expand height
472
+ clientW = canvas.parentElement.clientWidth;
473
+ clientH = Math.max(855, neededHeight);
474
+ canvas.style.width = clientW + 'px';
475
+ canvas.style.height = clientH + 'px';
476
+
477
+ // High-DPI Support
478
+ const dpr = window.devicePixelRatio || 1;
479
+
480
+ // Set physical size (resolution)
481
+ canvas.width = clientW * dpr;
482
+ canvas.height = clientH * dpr;
483
+ }
484
+
485
+ const w = canvas.width;
486
+ const h = canvas.height;
487
+ ctx.clearRect(0, 0, w, h);
488
+
489
+ // Reset node positions for this frame
490
+ window.nodePositions = [];
491
+
492
+ // Verify pan/zoom
493
+ ctx.save();
494
+
495
+ // Apply High-DPI Scale first
496
+ const dpr = window.devicePixelRatio || 1;
497
+ ctx.scale(dpr, dpr);
498
+
499
+ ctx.translate(window.panX, window.panY);
500
+ ctx.scale(window.vizScale, window.vizScale);
501
+
502
+ const layoutPad = 40;
503
+ // input, hidden(center), output
504
+ // Use Logical Width/Height for valid coordinates
505
+ const logicalW = canvas.width / dpr;
506
+ const logicalH = canvas.height / dpr;
507
+
508
+ const cx = logicalW / 2;
509
+ const cy = logicalH / 2;
510
+
511
+ // Horizontal layer centers (X)
512
+ const layerX_H = [cx - window.smoothLayerSpacing, cx, cx + window.smoothLayerSpacing];
513
+ // Vertical layer centers (Y)
514
+ const layerY_V = [cy - window.smoothLayerSpacing, cy, cy + window.smoothLayerSpacing];
515
+
516
+ function getPos(layerIdx, index, count, spacing) {
517
+ // Horizontal (Default)
518
+ const colH = (count - 1) * spacing;
519
+ const startY = (logicalH - colH) / 2;
520
+ const hX = layerX_H[layerIdx];
521
+ const hY = startY + index * spacing;
522
+
523
+ // Vertical
524
+ const colW = (count - 1) * spacing;
525
+ const startX = (logicalW - colW) / 2;
526
+ const vY = layerY_V[layerIdx];
527
+ const vX = startX + index * spacing;
528
+
529
+ // Blend
530
+ const f = window.smoothVerticalFactor;
531
+ return {
532
+ x: hX + (vX - hX) * f,
533
+ y: hY + (vY - hY) * f
534
+ };
535
+ }
536
+
537
+ // Draw Weights (Input -> Hidden)
538
+ for (let i = 0; i < iCount; i++) {
539
+ for (let j = 0; j < hCount; j++) {
540
+ // Visibility Check
541
+ let visible = true;
542
+ if (window.activeNode) {
543
+ const an = window.activeNode;
544
+ // Connects if: Active is Input(i) OR Active is Hidden(j)
545
+ const connected = (an.layer === 'input' && an.index === i) ||
546
+ (an.layer === 'hidden' && an.index === j);
547
+ if (!connected) visible = false;
548
+ }
549
+
550
+ // Draw Faded or Full
551
+ let baseAlpha = visible ? window.smoothEdgeOpacity : 0.02;
552
+ let alpha = baseAlpha;
553
+
554
+ const weight = weights.ih[j][i];
555
+
556
+ if (visible) {
557
+ // Smooth Blend for Opacity: Uniform vs Proportional
558
+ // Proportional: Scale alpha by weight magnitude (min 0.1)
559
+ const propAlpha = Math.min(baseAlpha, Math.max(0.1, Math.abs(weight) * baseAlpha));
560
+ // Blend: baseAlpha (Uniform) -> propAlpha (Proportional)
561
+ alpha = baseAlpha + (propAlpha - baseAlpha) * window.smoothPropOpacityFactor;
562
+
563
+ // Signal-Based Visibility (Active Edges)
564
+ // Signal = Source Node Activation * Weight
565
+ const rawVal = data.inputs[i];
566
+ const sourceVal = (rawVal && rawVal.val !== undefined) ? rawVal.val : rawVal;
567
+ const signal = Math.abs(sourceVal * weight);
568
+ // Signal Alpha: Scale by signal strength (boosted 3x for visibility)
569
+ const signalAlpha = Math.min(baseAlpha, signal * 3.0);
570
+
571
+ // Blend: Standard Alpha -> Signal Alpha based on smoothActiveFactor
572
+ alpha = alpha + (signalAlpha - alpha) * window.smoothActiveFactor;
573
+ }
574
+
575
+ if (window.smoothEdgeWidth > 0.05) {
576
+ ctx.beginPath();
577
+ const p1 = getPos(0, i, iCount, effSpacingInput);
578
+ const p2 = getPos(1, j, hCount, effSpacingHidden);
579
+
580
+ ctx.moveTo(p1.x, p1.y);
581
+
582
+ // Always use Bezier to allow interpolation (Straight <-> Curved)
583
+ const x1 = p1.x;
584
+ const y1 = p1.y;
585
+ const x2 = p2.x;
586
+ const y2 = p2.y;
587
+
588
+ // Bezier Factor: 0.0 (CPs at ends) -> 1.0 (CPs at midpoints)
589
+ const factor = window.smoothBezierFactor;
590
+
591
+ // Calculate control points based on orientation
592
+ const fV = window.smoothVerticalFactor;
593
+ const dx = (x2 - x1) * 0.5 * factor;
594
+ const dy = (y2 - y1) * 0.5 * factor;
595
+
596
+ // Blend CP offsets: Horizontal uses X offset, Vertical uses Y offset
597
+ const cx1 = x1 + dx * (1 - fV);
598
+ const cy1 = y1 + dy * fV;
599
+ const cx2 = x2 - dx * (1 - fV);
600
+ const cy2 = y2 - dy * fV;
601
+
602
+ ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);
603
+
604
+ // Smooth Weight Color Transition
605
+ const weightColorTarget = weight > 0 ? window.posColor : window.negColor;
606
+ const blendedColor = lerpColor(window.defaultEdgeColor, weightColorTarget, window.smoothWeightColorFactor, alpha);
607
+ ctx.strokeStyle = blendedColor;
608
+
609
+ // Smooth Blend: Uniform vs Proportional (Replaces above block visually if factor used)
610
+ const wUniform = window.smoothEdgeWidth;
611
+ const wProp = Math.abs(weight) * window.smoothEdgeWidth * 3;
612
+ ctx.lineWidth = wUniform + (wProp - wUniform) * window.smoothPropFactor;
613
+
614
+ ctx.stroke();
615
+ }
616
+ }
617
+ }
618
+
619
+ // Draw Weights (Hidden -> Output)
620
+ for (let j = 0; j < hCount; j++) {
621
+ for (let k = 0; k < oCount; k++) {
622
+ // Visibility Check
623
+ let visible = true;
624
+ if (window.activeNode) {
625
+ const an = window.activeNode;
626
+ const connected = (an.layer === 'hidden' && an.index === j) ||
627
+ (an.layer === 'output' && an.index === k);
628
+ if (!connected) visible = false;
629
+ }
630
+ let baseAlpha = visible ? window.smoothEdgeOpacity : 0.02;
631
+ let alpha = baseAlpha;
632
+
633
+ const weight = weights.ho[k][j];
634
+
635
+ if (visible) {
636
+ // Smooth Blend for Opacity: Uniform vs Proportional
637
+ const propAlpha = Math.min(baseAlpha, Math.max(0.1, Math.abs(weight) * baseAlpha));
638
+ alpha = baseAlpha + (propAlpha - baseAlpha) * window.smoothPropOpacityFactor;
639
+
640
+ // Signal-Based Visibility
641
+ const rawVal = data.hidden[j];
642
+ const sourceVal = (rawVal && rawVal.val !== undefined) ? rawVal.val : rawVal;
643
+ const signal = Math.abs(sourceVal * weight);
644
+ const signalAlpha = Math.min(baseAlpha, signal * 3.0);
645
+
646
+ alpha = alpha + (signalAlpha - alpha) * window.smoothActiveFactor;
647
+ }
648
+
649
+ if (window.smoothEdgeWidth > 0.05) {
650
+ ctx.beginPath();
651
+ const p1 = getPos(1, j, hCount, effSpacingHidden);
652
+ const p2 = getPos(2, k, oCount, effSpacingOutput);
653
+
654
+ ctx.moveTo(p1.x, p1.y);
655
+
656
+ // Always use Bezier to allow interpolation
657
+ const x1 = p1.x;
658
+ const y1 = p1.y;
659
+ const x2 = p2.x;
660
+ const y2 = p2.y;
661
+
662
+ const factor = window.smoothBezierFactor;
663
+
664
+ // Calculate control points based on orientation
665
+ const fV = window.smoothVerticalFactor;
666
+ const dx = (x2 - x1) * 0.5 * factor;
667
+ const dy = (y2 - y1) * 0.5 * factor;
668
+
669
+ // Blend CP offsets
670
+ const cx1 = x1 + dx * (1 - fV);
671
+ const cy1 = y1 + dy * fV;
672
+ const cx2 = x2 - dx * (1 - fV);
673
+ const cy2 = y2 - dy * fV;
674
+
675
+ ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);
676
+
677
+ // Smooth Weight Color Transition
678
+ const weightColorTarget = weight > 0 ? window.posColor : window.negColor;
679
+ const blendedColor = lerpColor(window.defaultEdgeColor, weightColorTarget, window.smoothWeightColorFactor, alpha);
680
+ ctx.strokeStyle = blendedColor;
681
+
682
+ // Smooth Blend: Uniform vs Proportional
683
+ const wUniform = window.smoothEdgeWidth;
684
+ const wProp = Math.abs(weight) * window.smoothEdgeWidth * 3;
685
+ ctx.lineWidth = wUniform + (wProp - wUniform) * window.smoothPropFactor;
686
+
687
+ ctx.stroke();
688
+ }
689
+ }
690
+ }
691
+
692
+
693
+
694
+
695
+
696
+ function drawLayerLabels(ctx, layerX_H, layerY_V, counts, logicalW, logicalH, contentHeight) {
697
+ ctx.save();
698
+ ctx.font = "12px Arial, sans-serif";
699
+ ctx.fillStyle = "rgb(84, 84, 84)";
700
+ const labels = ["Input Layer", "Hidden Layer", "Output Layer"];
701
+ const fV = window.smoothVerticalFactor;
702
+
703
+ const superscriptMap = {
704
+ '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
705
+ '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹'
706
+ };
707
+
708
+ for (let i = 0; i < 3; i++) {
709
+ const count = counts[i];
710
+ const superCount = count.toString().split('').map(c => superscriptMap[c] || c).join('');
711
+ const text = `${labels[i]} \u2208 \u211d${superCount}`;
712
+
713
+ // Horizontal Position
714
+ const hX = layerX_H[i];
715
+ const hY = (logicalH + contentHeight) / 2 + 40;
716
+
717
+ // Vertical Position
718
+ const vX = (logicalW + contentHeight) / 2 + 60;
719
+ const vY = layerY_V[i];
720
+
721
+ const finalX = hX + (vX - hX) * fV;
722
+ const finalY = hY + (vY - hY) * fV;
723
+
724
+ ctx.textAlign = fV > 0.5 ? "left" : "center";
725
+ ctx.fillText(text, finalX, finalY);
726
+ }
727
+ ctx.restore();
728
+ }
729
+
730
+ function drawLayer(count, activations, dummy_x, spacing, layerName, labels = null, biases = null) {
731
+ const radius = window.smoothRadius;
732
+ for (let i = 0; i < count; i++) {
733
+ try {
734
+ const pos = getPos(getLayerIdx(layerName), i, count, spacing);
735
+ const x = pos.x;
736
+ const y = pos.y;
737
+ let val = activations ? activations[i] : 0;
738
+
739
+ // Robust Handling: Flatten arrays if nested JS arrays arrive
740
+ if (Array.isArray(val)) val = val[0];
741
+ if (val === undefined || val === null || isNaN(val)) val = 0;
742
+
743
+ // --- Node Visibility Logic ---
744
+ let visible = true;
745
+ if (window.activeNode) {
746
+ const an = window.activeNode;
747
+ // Show if Self
748
+ if (an.layer === layerName && an.index === i) visible = true;
749
+ // Show if Neighbor
750
+ else if (layerName === 'input' && an.layer === 'hidden') visible = true; // Is it ANY input? technically all inputs connect to all hiddens
751
+ else if (layerName === 'hidden' && an.layer === 'input') visible = true;
752
+ else if (layerName === 'hidden' && an.layer === 'output') visible = true;
753
+ else if (layerName === 'output' && an.layer === 'hidden') visible = true;
754
+ else visible = false;
755
+
756
+ // Refined Neighbor Logic: only connected?
757
+ // Since full dense, all Input<->Hidden are connected. All Hidden<->Output are connected.
758
+ // So if I select Input3, ALL hidden nodes are neighbors.
759
+ // If I select Hidden5, ALL inputs and ALL outputs are neighbors.
760
+ // If I select Output2, ALL hidden nodes are neighbors.
761
+ // So logical filtering:
762
+ // If Active is Input: Show Active Input, All Hiddens.
763
+ // If Active is Hidden: Show All Inputs, Active Hidden, All Outputs.
764
+ // If Active is Output: Show All Hiddens, Active Output.
765
+
766
+ // Actually, simpler visual:
767
+ // Fade everything out.
768
+ // Always show Active node full.
769
+ // If Active is Input: Fade all other Inputs. Show Hiddens (neighbors) full.
770
+ // If Active is Output: Fade all other Outputs. Show Hiddens (neighbors) full.
771
+ // If Active is Hidden: Fade all other Hiddens. Show Inputs and Outputs full.
772
+
773
+ // Let's implement this simpler node visibility logic:
774
+ if (an.layer === layerName && an.index !== i) visible = false; // Hide siblings
775
+ // Neighbors are strictly in adjacent layers
776
+ if (Math.abs(getLayerIdx(layerName) - getLayerIdx(an.layer)) > 1) visible = false; // Hide non-neighbors
777
+ // Wait, Input(0) and Output(2) diff is 2. So correct.
778
+ }
779
+ const alpha = visible ? 1.0 : 0.1;
780
+
781
+ ctx.globalAlpha = alpha;
782
+
783
+ ctx.beginPath();
784
+ ctx.fillStyle = window.nodeColor;
785
+ ctx.lineWidth = 0.5;
786
+ ctx.strokeStyle = window.nodeBorderColor;
787
+
788
+ if (window.usePixelNodes) {
789
+ // Rasterized Circle (Round Pixel)
790
+ // Dynamic pSize for better roundness
791
+ let pSize = Math.max(1, Math.floor(radius / 4));
792
+
793
+ for (let dy = -radius; dy <= radius; dy += pSize) {
794
+ for (let dx = -radius; dx <= radius; dx += pSize) {
795
+ const dist = Math.sqrt(dx * dx + dy * dy);
796
+
797
+ if (dist <= radius) {
798
+ // Default to Border
799
+ ctx.fillStyle = window.nodeBorderColor;
800
+
801
+ // Inner Fill Logic (Radius - BorderThickness)
802
+ // Standard border is 0.5 lineWidth, here let's make it 1 pSize unit
803
+ if (dist <= radius - pSize) {
804
+ ctx.fillStyle = window.nodeColor;
805
+
806
+ // Activation Logic (Inner-most core)
807
+ // val is 0.0 to 1.0.
808
+ // If we are deep enough inside relative to activation size?
809
+ // Activation Radius = radius * 0.5 * val? No, just proportional fill.
810
+ // Original logic: radius * 0.5 + val * (radius * 0.5)
811
+ const actRadius = (radius * 0.5) + (val * radius * 0.5);
812
+ if (dist <= actRadius) {
813
+ // Blend Logic? Canvas doesn't support per-pixel alpha easily in this loop efficiently
814
+ // So just decide check:
815
+ // Activation color is black with alpha val.
816
+ // Here we just make it dark gray magnitude?
817
+ // Simpler: If within activation radius, draw activation color (black)
818
+ // Logic: inner fill is white. Activation is black overlay.
819
+ // If we are in activation zone, draw gray/black based on strength?
820
+ // Improved: If val > 0.1 and dist <= (radius * 0.8 * val), draw Black.
821
+ // Let's mimic original:
822
+ // Original draws Fill(NodeColor) THEN Fill(BlackAlpha).
823
+ // Here we pick ONE color for the pixel.
824
+
825
+ // Mix NodeColor and Black based on val?
826
+ // If NodeColor is White, result is Grey.
827
+ // Simply: If dist < actRadius, draw Darker.
828
+ if (dist <= radius * val) {
829
+ ctx.fillStyle = `rgba(0,0,0,${val})`;
830
+ // Note: rgba works with fillRect, it blends with canvas background (white? transparent?)
831
+ // But we are drawing pixel-by-pixel. Overlap?
832
+ // Actually, if we just set fillStyle to rgba(0,0,0,val) and draw rect
833
+ // over the canvas, it's fine.
834
+ // BUT we need to paint the NodeColor underneath first if transparency matters?
835
+ // Yes.
836
+ }
837
+ }
838
+ }
839
+
840
+ // Draw Pixel
841
+ // Snap to integer grid to avoid anti-aliasing artifacts
842
+ const px = Math.floor(x + dx);
843
+ const py = Math.floor(y + dy);
844
+
845
+ // 1. Draw Solid Base (Hides background lines)
846
+ // We must reset fillStyle to nodeColor or nodeBorderColor based on position
847
+ if (dist <= radius - pSize) {
848
+ ctx.fillStyle = window.nodeColor;
849
+ } else {
850
+ ctx.fillStyle = window.nodeBorderColor;
851
+ }
852
+ ctx.fillRect(px, py, pSize, pSize);
853
+
854
+ // 2. Draw Semi-Transparent Activation on Top
855
+ const absVal = Math.abs(val);
856
+ if (dist <= radius - pSize) {
857
+ if (dist <= radius * absVal) { // Math Fix
858
+ if (window.useNodeActivationColor) {
859
+ const color = val > 0 ? window.posColor : window.negColor;
860
+ ctx.fillStyle = color;
861
+ ctx.globalAlpha = Math.min(absVal, 1.0);
862
+ ctx.fillRect(px, py, pSize, pSize);
863
+ ctx.globalAlpha = 1.0;
864
+ } else {
865
+ ctx.fillStyle = `rgba(0, 0, 0, ${absVal})`;
866
+ ctx.fillRect(px, py, pSize, pSize);
867
+ }
868
+ }
869
+ }
870
+ }
871
+ }
872
+ }
873
+
874
+ } else {
875
+ // Original Vector Circle
876
+ ctx.fillStyle = window.nodeColor;
877
+ ctx.lineWidth = 0.5;
878
+ ctx.strokeStyle = window.nodeBorderColor;
879
+ ctx.beginPath();
880
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
881
+ ctx.fill();
882
+ ctx.stroke();
883
+
884
+ // Activation
885
+ const absVal = Math.abs(val);
886
+ // Clamp Radius Calculation
887
+ // Math Fix: Use absVal for sizing.
888
+ // Logic: Base inner circle is 0.5r. Grow to 1.0r with activation.
889
+ const actRadius = radius * 0.5 + Math.min(absVal, 1.0) * (radius * 0.5);
890
+
891
+ ctx.beginPath();
892
+ ctx.arc(x, y, actRadius, 0, Math.PI * 2);
893
+
894
+ if (window.useNodeActivationColor) {
895
+ // Color Mode
896
+ const color = val > 0 ? window.posColor : window.negColor;
897
+ ctx.fillStyle = color;
898
+ ctx.globalAlpha = Math.min(absVal, 1.0); // Use global alpha for hex colors
899
+ ctx.fill();
900
+ ctx.globalAlpha = 1.0;
901
+ } else {
902
+ // Grayscale Mode (Math Fix Applied)
903
+ ctx.fillStyle = `rgba(0,0,0,${Math.min(absVal, 1.0)})`;
904
+ ctx.fill();
905
+ }
906
+ }
907
+
908
+ // Bias Visualization (Indicator)
909
+ // Draw small dot/square on top-right rim
910
+ // Bias range is usually -0.5 to 0.5 or larger.
911
+
912
+ if (window.showBiases && biases && biases[i] !== undefined) {
913
+ const biasVal = Array.isArray(biases[i]) ? biases[i][0] : biases[i]; // Handle [val] format
914
+ const biasMag = Math.abs(biasVal);
915
+ // Color Logic: Red for Negative, Green for Positive
916
+ const biasColor = biasVal >= 0 ? "rgb(0, 255, 0)" : "rgb(255, 0, 0)";
917
+
918
+ if (window.usePixelNodes) {
919
+ // Pixel Indiciator on rim
920
+ const pSize = Math.max(1, Math.floor(radius / 4));
921
+ // Position: Top Right Corner relative to center
922
+ // 45 degrees. dx = r * 0.707
923
+ const offset = Math.floor(radius * 0.75);
924
+ const px = Math.floor(x + offset);
925
+ const py = Math.floor(y - offset);
926
+
927
+ ctx.fillStyle = biasColor;
928
+ ctx.fillRect(px, py, pSize * 2, pSize * 2); // Slightly larger dot
929
+ } else {
930
+ // Vector Indicator
931
+ const offset = radius * 0.8;
932
+ const bx = x + offset;
933
+ const by = y - offset;
934
+
935
+ ctx.beginPath();
936
+ ctx.arc(bx, by, radius * 0.3, 0, Math.PI * 2);
937
+ ctx.fillStyle = biasColor;
938
+ ctx.fill();
939
+ // Optional border for visibility?
940
+ // ctx.strokeStyle = "black";
941
+ // ctx.lineWidth = 0.5;
942
+ // ctx.stroke();
943
+ }
944
+ }
945
+
946
+ if (labels && labels[i]) {
947
+ ctx.save();
948
+ ctx.fillStyle = "rgb(84, 84, 84)";
949
+ ctx.font = "10px 'PressStart2P'";
950
+
951
+ const fV = window.smoothVerticalFactor;
952
+
953
+ // Target Positions & Rotation
954
+ // Horizontal (fV=0): x-12, y+4, rot=0
955
+ // Vertical (fV=1): x, y-20, rot=-90deg
956
+ const targetRot = -Math.PI / 2;
957
+ const currentRot = targetRot * fV;
958
+
959
+ const hX = x - 12;
960
+ const hY = y + 4;
961
+ const vX = x;
962
+ const vY = y - 20;
963
+
964
+ const finalX = hX + (vX - hX) * fV;
965
+ const finalY = hY + (vY - hY) * fV;
966
+
967
+ ctx.translate(finalX, finalY);
968
+ ctx.rotate(currentRot);
969
+
970
+ ctx.textAlign = fV > 0.5 ? "left" : "right";
971
+ ctx.fillText(labels[i] + " " + val.toFixed(2), 0, 0);
972
+
973
+ ctx.restore();
974
+ }
975
+
976
+ } catch (err) {
977
+ // Suppress error to allow other nodes to draw
978
+ // console.error("Node draw error:", err);
979
+ }
980
+ }
981
+ }
982
+
983
+ function getLayerIdx(name) {
984
+ if (name === 'input') return 0;
985
+ if (name === 'hidden') return 1;
986
+ if (name === 'output') return 2;
987
+ return -1;
988
+ }
989
+
990
+ drawLayer(iCount, data.inputs, null, effSpacingInput, 'input', ["Obj1 Dist", "Obj1 Act", "Obj1 W", "Obj2 Dist", "Obj2 Act", "Obj2 W", "Speed", "Gap", "DinoY", "DinoVel", "Air", "Duck"]);
991
+ drawLayer(hCount, data.hidden, null, effSpacingHidden, 'hidden', null, weights.bh);
992
+ drawLayer(oCount, data.outputs, null, effSpacingOutput, 'output', null, weights.bo);
993
+
994
+ // Draw Dynamic Layer Labels (Math Notation)
995
+ drawLayerLabels(ctx, layerX_H, layerY_V, [iCount, hCount, oCount], logicalW, logicalH, contentHeight);
996
+
997
+ // Labels Output
998
+ const outLabels = ["JUMP", "DUCK", "RUN"];
999
+ const fV = window.smoothVerticalFactor;
1000
+
1001
+ for (let i = 0; i < oCount; i++) {
1002
+ const pos = getPos(2, i, oCount, effSpacingOutput);
1003
+ ctx.save();
1004
+ ctx.fillStyle = "rgb(84, 84, 84)";
1005
+ ctx.font = "10px 'PressStart2P'";
1006
+
1007
+ const fV = window.smoothVerticalFactor;
1008
+
1009
+ // Target Positions & Rotation
1010
+ // Horizontal (fV=0): x+15, y+4, rot=0
1011
+ // Vertical (fV=1): x, y+25, rot=90deg
1012
+ const targetRot = Math.PI / 2;
1013
+ const currentRot = targetRot * fV;
1014
+
1015
+ const hX = pos.x + 15;
1016
+ const hY = pos.y + 4;
1017
+ const vX = pos.x;
1018
+ const vY = pos.y + 30;
1019
+
1020
+ const finalX = hX + (vX - hX) * fV;
1021
+ const finalY = hY + (vY - hY) * fV;
1022
+
1023
+ ctx.translate(finalX, finalY);
1024
+ ctx.rotate(currentRot);
1025
+
1026
+ ctx.textAlign = "left";
1027
+ ctx.fillText(outLabels[i] + " " + Math.round(data.outputs[i] * 100) + "%", 0, 0);
1028
+ ctx.restore();
1029
+ }
1030
+
1031
+ ctx.restore();
1032
+ }
1033
+
1034
+ // Start loop
1035
+ requestAnimationFrame(renderLoop);
1036
+ }
pydino/assets/100-offline-sprite.png ADDED
pydino/assets/200-offline-sprite.png ADDED
pydino/background_el.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # background_el.py
2
+ # 1:1 pygame port of Chrome Dino background_el.ts
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Optional, Dict, Any
8
+
9
+ import pygame
10
+
11
+ try:
12
+ from constants import IS_HIDPI
13
+ from utils import get_random_num
14
+ except ImportError:
15
+ from .constants import IS_HIDPI
16
+ from .utils import get_random_num # alias -> utils.get_random_num
17
+
18
+
19
+ # -----------------------------------------------------------------------------
20
+ # Config dataclasses (TS interface eşdeğerleri)
21
+ # -----------------------------------------------------------------------------
22
+ @dataclass
23
+ class BackgroundElSpriteConfig:
24
+ height: int
25
+ width: int
26
+ offset: int
27
+ xPos: int
28
+ fixed: bool
29
+ fixedXPos: Optional[int] = None
30
+ fixedYPos1: Optional[int] = None
31
+ fixedYPos2: Optional[int] = None
32
+
33
+
34
+ @dataclass
35
+ class BackgroundElConfig:
36
+ maxBgEls: int
37
+ maxGap: int
38
+ minGap: int
39
+ pos: int
40
+ speed: float
41
+ yPos: int
42
+ msPerFrame: int = 0 # only needed when spriteConfig.fixed is true
43
+
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Global config (TS: module-level mutable)
47
+ # -----------------------------------------------------------------------------
48
+ _global_config = BackgroundElConfig(
49
+ maxBgEls=0,
50
+ maxGap=0,
51
+ minGap=0,
52
+ msPerFrame=0,
53
+ pos=0,
54
+ speed=0.0,
55
+ yPos=0,
56
+ )
57
+
58
+
59
+ def get_global_config() -> BackgroundElConfig:
60
+ return _global_config
61
+
62
+
63
+ def set_global_config(config: BackgroundElConfig) -> None:
64
+ global _global_config
65
+ _global_config = config
66
+
67
+
68
+ # TS-style aliases (opsiyonel)
69
+ getGlobalConfig = get_global_config
70
+ setGlobalConfig = set_global_config
71
+
72
+
73
+ # -----------------------------------------------------------------------------
74
+ # Background element
75
+ # -----------------------------------------------------------------------------
76
+ class BackgroundEl:
77
+ gap: int
78
+ xPos: float
79
+ remove: bool
80
+
81
+ def __init__(
82
+ self,
83
+ canvas: pygame.Surface,
84
+ spritePos: Dict[str, int], # SpritePosition {x,y}
85
+ containerWidth: int,
86
+ type_name: str,
87
+ imageSpriteProvider: Any, # ImageSpriteProvider
88
+ ) -> None:
89
+ """
90
+ Background item. Similar to cloud, without random y position.
91
+ """
92
+ self.canvas: pygame.Surface = canvas
93
+ self.canvasCtx: pygame.Surface = canvas
94
+ self.spritePos = spritePos
95
+ self.imageSpriteProvider = imageSpriteProvider
96
+
97
+ self.xPos = float(containerWidth)
98
+ self.type = type_name
99
+ self.remove = False
100
+
101
+ # Rastgele gap
102
+ gc = get_global_config()
103
+ self.gap = getRandomNum(gc.minGap, gc.maxGap)
104
+
105
+ # Sprite config'i provider'dan al
106
+ sprite_def = imageSpriteProvider.getSpriteDefinition()
107
+ assert sprite_def is not None
108
+ cfg_raw = sprite_def["backgroundEl"][self.type]
109
+ # cfg_raw dict → dataclass'a sar
110
+ self.spriteConfig = BackgroundElSpriteConfig(
111
+ height=int(cfg_raw["height"]),
112
+ width=int(cfg_raw["width"]),
113
+ offset=int(cfg_raw["offset"]),
114
+ xPos=int(cfg_raw["xPos"]),
115
+ fixed=bool(cfg_raw["fixed"]),
116
+ fixedXPos=cfg_raw.get("fixedXPos"),
117
+ fixedYPos1=cfg_raw.get("fixedYPos1"),
118
+ fixedYPos2=cfg_raw.get("fixedYPos2"),
119
+ )
120
+
121
+ # Y konumu / sabit x ayarı
122
+ self.yPos: float = 0.0
123
+ self.animTimer: float = 0.0
124
+ self.switchFrames: bool = False
125
+
126
+ self.init()
127
+
128
+ # ------------------------------------------------------------------
129
+ # TS parity methods
130
+ # ------------------------------------------------------------------
131
+ def init(self) -> None:
132
+ if self.spriteConfig.fixed and self.spriteConfig.fixedXPos is not None:
133
+ self.xPos = float(self.spriteConfig.fixedXPos)
134
+
135
+ gc = get_global_config()
136
+ # yPos = global.yPos - height + offset
137
+ self.yPos = float(gc.yPos - self.spriteConfig.height + self.spriteConfig.offset)
138
+
139
+ self.draw()
140
+
141
+ def draw(self) -> None:
142
+ imageSprite: pygame.Surface = self.imageSpriteProvider.getRunnerImageSprite()
143
+ assert imageSprite is not None
144
+
145
+ # Kaynak kare (sprite sheet üzerinde)
146
+ sourceWidth = self.spriteConfig.width
147
+ sourceHeight = self.spriteConfig.height
148
+ sourceX = self.spriteConfig.xPos
149
+ sourceY = self.spritePos["y"]
150
+
151
+ scale = 2 if IS_HIDPI else 1
152
+ sx = sourceX * scale
153
+ sy = sourceY * scale
154
+ sw = sourceWidth * scale
155
+ sh = sourceHeight * scale
156
+
157
+ src_rect = pygame.Rect(sx, sy, sw, sh)
158
+ frame = imageSprite.subsurface(src_rect).copy()
159
+
160
+ # Hedef boyut: lo-dpi mantıksal piksel (TS çizimde output=sourceWidth/Height;
161
+ # pygame tarafında HiDPI için 2x örneklediğimiz kareyi mantıksal boyuta indiriyoruz)
162
+ if frame.get_width() != sourceWidth or frame.get_height() != sourceHeight:
163
+ frame = pygame.transform.scale(frame, (sourceWidth, sourceHeight))
164
+
165
+ # Blit
166
+ self.canvasCtx.blit(frame, (int(self.xPos), int(self.yPos)))
167
+
168
+ def update(self, speed: float) -> None:
169
+ if self.remove:
170
+ return
171
+
172
+ if self.spriteConfig.fixed:
173
+ gc = get_global_config()
174
+ # Animasyon zamanlayıcı (msPerFrame zorunlu)
175
+ assert gc.msPerFrame is not None
176
+ self.animTimer += speed
177
+ if self.animTimer > gc.msPerFrame:
178
+ self.animTimer = 0.0
179
+ self.switchFrames = not self.switchFrames
180
+
181
+ if self.spriteConfig.fixedYPos1 is not None and self.spriteConfig.fixedYPos2 is not None:
182
+ self.yPos = float(self.spriteConfig.fixedYPos1 if self.switchFrames else self.spriteConfig.fixedYPos2)
183
+ else:
184
+ # Sabit hız (oyun hızından bağımsız)
185
+ self.xPos -= get_global_config().speed
186
+
187
+ self.draw()
188
+
189
+ # Canvas dışında kaldı mı?
190
+ if not self.isVisible():
191
+ self.remove = True
192
+
193
+ def isVisible(self) -> bool:
194
+ # Sağ kenar > 0 ise görünür
195
+ return (self.xPos + self.spriteConfig.width) > 0
pydino/cloud.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cloud.py
2
+ # 1:1 pygame port of Chrome Dino cloud.ts
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+ from typing import Any, Dict
8
+
9
+ import pygame
10
+
11
+ try:
12
+ from constants import IS_HIDPI
13
+ from utils import get_random_num
14
+ except ImportError:
15
+ from .constants import IS_HIDPI
16
+ from .utils import get_random_num
17
+
18
+
19
+ class CloudConfig:
20
+ HEIGHT = 14
21
+ MAX_CLOUD_GAP = 400
22
+ MAX_SKY_LEVEL = 30
23
+ MIN_CLOUD_GAP = 100
24
+ MIN_SKY_LEVEL = 71
25
+ WIDTH = 46
26
+
27
+
28
+ def _pos(obj: Dict[str, int] | Any, key: str) -> int:
29
+ """Accept both dict-like {'x':...} and attr-like .x."""
30
+ if isinstance(obj, dict):
31
+ return int(obj[key])
32
+ return int(getattr(obj, key))
33
+
34
+
35
+ class Cloud:
36
+ """Cloud background item.
37
+ Obstacle'a benzer ama çarpışma kutusu yok.
38
+ """
39
+
40
+ # TS: public fields
41
+ gap: int
42
+ xPos: float
43
+ remove: bool = False
44
+
45
+ # TS: private yPos/canvasCtx/spritePos/imageSpriteProvider
46
+ _yPos: float
47
+ _canvas: pygame.Surface
48
+ _spritePos: Dict[str, int] | Any
49
+ _imageSpriteProvider: Any
50
+
51
+ def __init__(
52
+ self,
53
+ canvas: pygame.Surface,
54
+ sprite_pos: Dict[str, int] | Any,
55
+ container_width: int,
56
+ image_sprite_provider: Any, # ImageSpriteProvider
57
+ ) -> None:
58
+ self._canvas = canvas
59
+ self._spritePos = sprite_pos
60
+ self._imageSpriteProvider = image_sprite_provider
61
+
62
+ self.xPos = float(container_width)
63
+ self.gap = get_random_num(CloudConfig.MIN_CLOUD_GAP, CloudConfig.MAX_CLOUD_GAP)
64
+ self.remove = False
65
+ self._yPos = 0.0
66
+
67
+ self.init()
68
+
69
+ # ------------------------------------------------------------------ #
70
+ # TS: init() — bulutun yüksekliğini belirle ve çiz
71
+ # ------------------------------------------------------------------ #
72
+ def init(self) -> None:
73
+ self._yPos = float(
74
+ get_random_num(CloudConfig.MAX_SKY_LEVEL, CloudConfig.MIN_SKY_LEVEL)
75
+ )
76
+ self.draw()
77
+
78
+ # ------------------------------------------------------------------ #
79
+ # TS: draw()
80
+ # ------------------------------------------------------------------ #
81
+ def draw(self) -> None:
82
+ runner_image_sprite: pygame.Surface = self._imageSpriteProvider.getRunnerImageSprite()
83
+ assert runner_image_sprite is not None, "Runner image sprite is required"
84
+
85
+ source_w = CloudConfig.WIDTH
86
+ source_h = CloudConfig.HEIGHT
87
+ output_w = source_w
88
+ output_h = source_h
89
+
90
+ # HiDPI ise kaynak 2x
91
+ if IS_HIDPI:
92
+ source_w *= 2
93
+ source_h *= 2
94
+
95
+ src_x = _pos(self._spritePos, "x")
96
+ src_y = _pos(self._spritePos, "y")
97
+
98
+ src_rect = pygame.Rect(src_x, src_y, source_w, source_h)
99
+ frame = runner_image_sprite.subsurface(src_rect).copy()
100
+ if frame.get_size() != (output_w, output_h):
101
+ frame = pygame.transform.scale(frame, (output_w, output_h))
102
+
103
+ self._canvas.blit(frame, (int(self.xPos), int(self._yPos)))
104
+
105
+ # ------------------------------------------------------------------ #
106
+ # TS: update(speed)
107
+ # ------------------------------------------------------------------ #
108
+ def update(self, speed: float) -> None:
109
+ if not self.remove:
110
+ self.xPos -= math.ceil(speed)
111
+ self.draw()
112
+
113
+ if not self.isVisible():
114
+ self.remove = True
115
+
116
+ # ------------------------------------------------------------------ #
117
+ # TS: isVisible()
118
+ # ------------------------------------------------------------------ #
119
+ def isVisible(self) -> bool:
120
+ return (self.xPos + CloudConfig.WIDTH) > 0
pydino/constants.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # constants.py
2
+ # 1:1 port of Chrome Dino constants.ts for a pygame project.
3
+
4
+ from __future__ import annotations
5
+ import os
6
+ import sys
7
+ from dimensions import Dimensions # width, height dataclass
8
+
9
+ # Browser-only bayrakların pygame eşleniği:
10
+ # - IS_IOS / IS_MOBILE: masaüstünde genelde False. İstersen ENV ile zorlayabilirsin.
11
+ # - IS_HIDPI: tarayıcıdaki devicePixelRatio yerine ENV kullanıyoruz (varsayılan False).
12
+ # - IS_RTL: DOM yok; ister ENV ile ayarla, ister sabit False kalsın.
13
+
14
+ def _env_flag(name: str, default: bool = False) -> bool:
15
+ val = os.environ.get(name)
16
+ if val is None:
17
+ return default
18
+ return val.strip().lower() in ("1", "true", "yes", "on")
19
+
20
+ # TODO(salg): Use preprocessor to filter IOS code at build time. (TS notunu koruyoruz)
21
+ IS_IOS: bool = _env_flag("NEURODINO_IOS", default=("ios" in sys.platform))
22
+ IS_HIDPI: bool = _env_flag("NEURODINO_HIDPI", default=False)
23
+ IS_MOBILE: bool = _env_flag(
24
+ "NEURODINO_MOBILE",
25
+ default=("android" in sys.platform or IS_IOS),
26
+ )
27
+ IS_RTL: bool = _env_flag("NEURODINO_RTL", default=False)
28
+
29
+ # Frames per second.
30
+ FPS: int = 60
31
+
32
+ # Default logical dimensions of the game surface.
33
+ DEFAULT_DIMENSIONS: Dimensions = Dimensions(width=600, height=150)
pydino/debug_overlay.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # debug_overlay.py
2
+ import pygame
3
+
4
+ def _get(obj, name, default=None):
5
+ """Safe attribute/key getter: works with objects or dicts."""
6
+ if hasattr(obj, name):
7
+ return getattr(obj, name)
8
+ if isinstance(obj, dict):
9
+ return obj.get(name, default)
10
+ return default
11
+
12
+ class CollisionDebugOverlay:
13
+ def __init__(self):
14
+ # Varsayılan kapalı; F3 ile toggle
15
+ self.enabled = False
16
+
17
+ def draw(self, screen: pygame.Surface, trex, obstacles: list):
18
+ if not self.enabled or trex is None:
19
+ return
20
+
21
+ # --- T-Rex ana hitbox
22
+ tw = int(_get(trex.config, "width", 44))
23
+ th = int(_get(trex.config, "height", 47))
24
+ t_rect = pygame.Rect(
25
+ int(trex.xPos) + 1,
26
+ int(trex.yPos) + 1,
27
+ max(1, tw - 2),
28
+ max(1, th - 2),
29
+ )
30
+ pygame.draw.rect(screen, (0, 255, 0), t_rect, 1)
31
+
32
+ # --- T-Rex parçalı kutular
33
+ for b in trex.getCollisionBoxes():
34
+ r = pygame.Rect(
35
+ int(trex.xPos) + 1 + int(b.x),
36
+ int(trex.yPos) + 1 + int(b.y),
37
+ int(b.width),
38
+ int(b.height),
39
+ )
40
+ pygame.draw.rect(screen, (0, 200, 0), r, 1)
41
+
42
+ if not obstacles:
43
+ return
44
+
45
+ # --- İlk engelin ana hitbox'ı (istersen tümünü dolaşabilirsin)
46
+ ob = obstacles[0]
47
+ ow = int(_get(ob.typeConfig, "width", 0))
48
+ oh = int(_get(ob.typeConfig, "height", 0))
49
+ o_rect = pygame.Rect(
50
+ int(ob.xPos) + 1,
51
+ int(ob.yPos) + 1,
52
+ max(1, ow * int(getattr(ob, "size", 1)) - 2),
53
+ max(1, oh - 2),
54
+ )
55
+ pygame.draw.rect(screen, (255, 0, 0), o_rect, 1)
56
+
57
+ # --- Engelin parçalı kutuları
58
+ for b in getattr(ob, "collisionBoxes", []):
59
+ r = pygame.Rect(
60
+ int(ob.xPos) + 1 + int(b.x),
61
+ int(ob.yPos) + 1 + int(b.y),
62
+ int(b.width),
63
+ int(b.height),
64
+ )
65
+ pygame.draw.rect(screen, (200, 0, 0), r, 1)
pydino/dimensions.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # dimensions.py
2
+ # 1:1 port of dimensions.ts (Chrome Dino)
3
+ from dataclasses import dataclass
4
+
5
+ @dataclass
6
+ class Dimensions:
7
+ width: int
8
+ height: int
pydino/distance_meter.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # distance_meter.py
2
+ # 1:1 pygame port of Chrome Dino distance_meter.ts
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import List, Optional, Tuple, Any
7
+
8
+ import pygame
9
+
10
+ try:
11
+ from constants import IS_HIDPI, IS_RTL
12
+ except ImportError:
13
+ from .constants import IS_HIDPI, IS_RTL
14
+ from offline_sprite_definitions import CollisionBox
15
+ # SpritePosition: dict-like {"x": int, "y": int}
16
+ from utils import getTimeStamp
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Dimensions / Config (TS'deki enum sabitleri)
21
+ # ---------------------------------------------------------------------------
22
+ class _Dim:
23
+ WIDTH = 10 # tek rakam kaynağındaki genişlik
24
+ HEIGHT = 13 # tek rakam kaynağındaki yükseklik
25
+ DEST_WIDTH = 11 # hedefte rakamlar arası adım
26
+
27
+
28
+ class _Cfg:
29
+ MAX_DISTANCE_UNITS = 5
30
+ ACHIEVEMENT_DISTANCE = 100
31
+ COEFFICIENT = 0.025
32
+ FLASH_DURATION = 1000 / 4 # ms
33
+ FLASH_ITERATIONS = 3
34
+ HIGH_SCORE_HIT_AREA_PADDING = 4
35
+
36
+
37
+ class DistanceMeter:
38
+ achievement: bool = False
39
+
40
+ def __init__(self, canvas: pygame.Surface, spritePos: dict, canvasWidth: int,
41
+ imageSpriteProvider: Any) -> None:
42
+ """Handles displaying the distance meter."""
43
+ self.canvas = canvas
44
+ self.canvasCtx = canvas # pygame.Surface
45
+ self.image: pygame.Surface = imageSpriteProvider.getRunnerImageSprite()
46
+ self.spritePos = spritePos # {"x": int, "y": int}
47
+
48
+ self.x: int = 0
49
+ self.y: int = 5
50
+ self.maxScore: int = 0
51
+ self.highScore: str = ""
52
+ self._show_high_score: bool = False
53
+ self.digits: List[str] = []
54
+ self.defaultString: str = ""
55
+ self.flashTimer: float = 0.0
56
+ self.flashIterations: int = 0
57
+
58
+ self.flashingRafId = None # TS uyumu için placeholder
59
+ self.highScoreBounds: Optional[CollisionBox] = None
60
+ self.highScoreFlashing: bool = False
61
+ self.maxScoreUnits: int = _Cfg.MAX_DISTANCE_UNITS
62
+ self.canvasWidth: int = canvasWidth
63
+ self.frameTimeStamp: Optional[int] = None
64
+
65
+ self.init(canvasWidth)
66
+
67
+ # -----------------------------------------------------------------------
68
+ # TS parity methods
69
+ # -----------------------------------------------------------------------
70
+ def init(self, width: int) -> None:
71
+ """Initialise the distance meter to '00000'."""
72
+ maxDistanceStr = ""
73
+ self.calcXpos(width)
74
+ self.maxScore = self.maxScoreUnits
75
+ for i in range(self.maxScoreUnits):
76
+ self.draw(i, 0)
77
+ self.defaultString += "0"
78
+ maxDistanceStr += "9"
79
+ self.maxScore = int(maxDistanceStr, 10)
80
+
81
+ def calcXpos(self, canvasWidth: int) -> None:
82
+ """Calculate the xPos in the canvas."""
83
+ self.x = canvasWidth - (_Dim.DEST_WIDTH * (self.maxScoreUnits + 1))
84
+
85
+ def _blit_sprite_digit(self, source_x: int, source_y: int, source_w: int, source_h: int,
86
+ dest_x: int, dest_y: int, dest_w: int, dest_h: int) -> None:
87
+ """Helper: copy region from sprite sheet and blit to canvas, handling HiDPI."""
88
+ scale = 2 if IS_HIDPI else 1
89
+ sx = source_x * scale
90
+ sy = source_y * scale
91
+ sw = source_w * scale
92
+ sh = source_h * scale
93
+
94
+ src_rect = pygame.Rect(sx, sy, sw, sh)
95
+ frame = self.image.subsurface(src_rect).copy()
96
+
97
+ # hedef boyut (lo-dpi mantığı) sabit: WIDTH/HEIGHT
98
+ if frame.get_width() != dest_w or frame.get_height() != dest_h:
99
+ frame = pygame.transform.scale(frame, (dest_w, dest_h))
100
+
101
+ self.canvasCtx.blit(frame, (dest_x, dest_y))
102
+
103
+ def draw(self, digitPos: int, value: int, highScore: bool = False) -> None:
104
+ """Draw a digit to canvas."""
105
+ sourceWidth = _Dim.WIDTH
106
+ sourceHeight = _Dim.HEIGHT
107
+ sourceX = _Dim.WIDTH * value
108
+ sourceY = 0
109
+
110
+ # Hedefte rakamlar dEST_WIDTH ile yan yana
111
+ targetX = digitPos * _Dim.DEST_WIDTH
112
+ targetY = self.y
113
+ targetWidth = _Dim.WIDTH
114
+ targetHeight = _Dim.HEIGHT
115
+
116
+ # Kaynak sprite sheet ofsetleri
117
+ sourceX += int(self.spritePos["x"])
118
+ sourceY += int(self.spritePos["y"])
119
+
120
+ # RTL konumlama (TS'deki translate + scale yerine mutlak X hesaplıyoruz)
121
+ if IS_RTL:
122
+ if highScore:
123
+ translateX = self.canvasWidth - (_Dim.WIDTH * (self.maxScoreUnits + 3))
124
+ else:
125
+ translateX = self.canvasWidth - _Dim.WIDTH
126
+ dest_x = translateX - targetX # ayna etkisi nedeniyle ters
127
+ else:
128
+ highScoreX = self.x - (self.maxScoreUnits * 2) * _Dim.WIDTH
129
+ dest_base = highScoreX if highScore else self.x
130
+ dest_x = dest_base + targetX
131
+
132
+ dest_y = targetY
133
+
134
+ self._blit_sprite_digit(sourceX, sourceY, sourceWidth, sourceHeight,
135
+ dest_x, dest_y, targetWidth, targetHeight)
136
+
137
+ def getActualDistance(self, distance: float) -> int:
138
+ """Convert pixel distance to a 'real' distance."""
139
+ return int(round(distance * _Cfg.COEFFICIENT)) if distance else 0
140
+
141
+ def update(self, deltaTime: float, distance: float) -> bool:
142
+ """Update the distance meter. Returns whether achievement sound should play."""
143
+ paint = True
144
+ playSound = False
145
+
146
+ if not self.achievement:
147
+ distance_real = self.getActualDistance(distance)
148
+
149
+ # Score genişlerse bir haneyi arttır
150
+ if distance_real > self.maxScore and self.maxScoreUnits == _Cfg.MAX_DISTANCE_UNITS:
151
+ self.maxScoreUnits += 1
152
+ self.maxScore = int(f"{self.maxScore}9", 10)
153
+
154
+ if distance_real > 0:
155
+ # Achievement
156
+ if distance_real % _Cfg.ACHIEVEMENT_DISTANCE == 0:
157
+ self.achievement = True
158
+ self.flashTimer = 0.0
159
+ playSound = True
160
+
161
+ # '00042' gibi sol sıfırlarla dizgi
162
+ s = (self.defaultString + str(distance_real))[-self.maxScoreUnits:]
163
+ self.digits = list(s)
164
+ else:
165
+ self.digits = list(self.defaultString)
166
+ else:
167
+ # Achievement flashing
168
+ if self.flashIterations <= _Cfg.FLASH_ITERATIONS:
169
+ self.flashTimer += deltaTime
170
+ if self.flashTimer < _Cfg.FLASH_DURATION:
171
+ paint = False
172
+ elif self.flashTimer > _Cfg.FLASH_DURATION * 2:
173
+ self.flashTimer = 0.0
174
+ self.flashIterations += 1
175
+ else:
176
+ self.achievement = False
177
+ self.flashIterations = 0
178
+ self.flashTimer = 0.0
179
+
180
+ # Draw digits
181
+ if paint:
182
+ for i in range(len(self.digits) - 1, -1, -1):
183
+ self.draw(i, int(self.digits[i]))
184
+
185
+ # High score daima üstüne çizilir
186
+ self.drawHighScore()
187
+
188
+ # (TS: flashHighScore ayrı rAF ile çalışır) — burada da ilerletelim:
189
+ if self.highScoreFlashing:
190
+ self._step_high_score_flash(deltaTime)
191
+
192
+ return playSound
193
+
194
+ def drawHighScore(self) -> None:
195
+ if not getattr(self, "_show_high_score", False) or len(self.highScore) == 0:
196
+ return
197
+
198
+ # Yüksek skoru biraz saydam çiz
199
+ # pygame: global alpha yok; yüzeyi geçici modla doldurmak yerine direkt çiziyoruz
200
+ # (görsel eşleşme tam değil ama işlev aynı)
201
+ for i in range(len(self.highScore) - 1, -1, -1):
202
+ ch = self.highScore[i]
203
+ # 0-9 normal; 'H'->10, 'I'->11
204
+ if ch.isdigit():
205
+ pos = int(ch)
206
+ elif ch == "H":
207
+ pos = 10
208
+ elif ch == "I":
209
+ pos = 11
210
+ else:
211
+ continue
212
+ self.draw(i, pos, True)
213
+
214
+ def setHighScore(self, distance: float) -> None:
215
+ distance_real = self.getActualDistance(distance)
216
+ s = (self.defaultString + str(distance_real))[-self.maxScoreUnits:]
217
+ self.highScore = "HI " + s
218
+ self._show_high_score = True
219
+
220
+ # -----------------------------------------------------------------------
221
+ # High score mouse/touch alanı ve flashing
222
+ # -----------------------------------------------------------------------
223
+ def hasClickedOnHighScore(self, e: Any) -> bool:
224
+ """Pygame uyarlaması: e.pos veya (x,y) tuple bekler."""
225
+ if isinstance(e, tuple) and len(e) == 2:
226
+ x, y = e
227
+ elif hasattr(e, "pos"):
228
+ x, y = getattr(e, "pos")
229
+ else:
230
+ # Diğer event tipleri desteklenmiyor; dışarıdan (x,y) ile çağır.
231
+ return False
232
+
233
+ self.highScoreBounds = self.getHighScoreBounds()
234
+ hb = self.highScoreBounds
235
+ return (x >= hb.x and x <= hb.x + hb.width and
236
+ y >= hb.y and y <= hb.y + hb.height)
237
+
238
+ def getHighScoreBounds(self) -> CollisionBox:
239
+ return CollisionBox(
240
+ x=(self.x - (self.maxScoreUnits * 2) * _Dim.WIDTH) - _Cfg.HIGH_SCORE_HIT_AREA_PADDING,
241
+ y=self.y,
242
+ width=_Dim.WIDTH * (len(self.highScore) + 1) + _Cfg.HIGH_SCORE_HIT_AREA_PADDING,
243
+ height=_Dim.HEIGHT + (_Cfg.HIGH_SCORE_HIT_AREA_PADDING * 2),
244
+ )
245
+
246
+ def _step_high_score_flash(self, deltaTime: float) -> None:
247
+ """TS'deki requestAnimationFrame döngüsünü deltaTime ile taklit eder."""
248
+ now = getTimeStamp()
249
+ prev = self.frameTimeStamp or now
250
+ self.frameTimeStamp = now
251
+
252
+ # Reached max flashes?
253
+ if self.flashIterations > _Cfg.FLASH_ITERATIONS * 2:
254
+ self.cancelHighScoreFlashing()
255
+ return
256
+
257
+ paint = True
258
+ self.flashTimer += (now - prev) if deltaTime == 0 else deltaTime
259
+ if self.flashTimer < _Cfg.FLASH_DURATION:
260
+ paint = False
261
+ elif self.flashTimer > _Cfg.FLASH_DURATION * 2:
262
+ self.flashTimer = 0.0
263
+ self.flashIterations += 1
264
+
265
+ if paint:
266
+ self.drawHighScore()
267
+ else:
268
+ self.clearHighScoreBounds()
269
+
270
+ def clearHighScoreBounds(self) -> None:
271
+ if not self.highScoreBounds:
272
+ self.highScoreBounds = self.getHighScoreBounds()
273
+ hb = self.highScoreBounds
274
+ rect = pygame.Rect(hb.x, hb.y, hb.width, hb.height)
275
+ pygame.draw.rect(self.canvasCtx, (247, 247, 247), rect)
276
+
277
+ def startHighScoreFlashing(self) -> None:
278
+ self.highScoreFlashing = True
279
+ # TS rAF yerine zaman damgası başlat
280
+ self.frameTimeStamp = getTimeStamp()
281
+ self.flashTimer = 0.0
282
+ self.flashIterations = 0
283
+
284
+ def isHighScoreFlashing(self) -> bool:
285
+ return self.highScoreFlashing
286
+
287
+ def cancelHighScoreFlashing(self) -> None:
288
+ self.flashIterations = 0
289
+ self.flashTimer = 0.0
290
+ self.highScoreFlashing = False
291
+ self.clearHighScoreBounds()
292
+ self.drawHighScore()
293
+
294
+ def resetHighScore(self) -> None:
295
+ self.highScore = ""
296
+ self._show_high_score = False
297
+ self.cancelHighScoreFlashing()
298
+
299
+ def reset(self) -> None:
300
+ """Reset the distance meter back to '00000'."""
301
+ self.update(0, 0)
302
+ self.achievement = False
pydino/game_over_panel.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # game_over_panel.py
2
+ # 1:1 pygame port of Chrome Dino game_over_panel.ts
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional, Any, Dict
7
+
8
+ import pygame
9
+
10
+ try:
11
+ from constants import IS_HIDPI, IS_RTL
12
+ except ImportError:
13
+ from .constants import IS_HIDPI, IS_RTL
14
+ from dimensions import Dimensions
15
+ from offline_sprite_definitions import sprite_definition_by_type
16
+ from utils import getTimeStamp
17
+
18
+
19
+ RESTART_ANIM_DURATION: int = 875
20
+ LOGO_PAUSE_DURATION: int = 875
21
+ FLASH_ITERATIONS: int = 5
22
+
23
+
24
+ class _AnimConfig:
25
+ frames = [0, 36, 72, 108, 144, 180, 216, 252]
26
+ msPerFrame = RESTART_ANIM_DURATION / 8
27
+
28
+
29
+ class _DefaultPanelDims:
30
+ # TS: GameOverPanelDimensions
31
+ textX = 0
32
+ textY = 13
33
+ textWidth = 191
34
+ textHeight = 11
35
+ restartWidth = 36
36
+ restartHeight = 32
37
+
38
+
39
+ class GameOverPanel:
40
+ def __init__(
41
+ self,
42
+ canvas: pygame.Surface,
43
+ text_img_pos: Dict[str, int], # SpritePosition: {"x": int, "y": int}
44
+ restart_img_pos: Dict[str, int], # SpritePosition
45
+ dimensions: Dimensions,
46
+ image_sprite_provider: Any,
47
+ alt_game_end_img_pos: Optional[Dict[str, int]] = None, # SpritePosition
48
+ alt_game_active: Optional[bool] = None,
49
+ ) -> None:
50
+ self.canvasCtx: pygame.Surface = canvas
51
+ self.canvasDimensions: Dimensions = dimensions
52
+ self.textImgPos = text_img_pos
53
+ self.restartImgPos = restart_img_pos
54
+ self.imageSpriteProvider = image_sprite_provider
55
+ self.altGameEndImgPos = alt_game_end_img_pos
56
+ self.altGameModeActive: bool = bool(alt_game_active)
57
+
58
+ self.frameTimeStamp: int = 0
59
+ self.animTimer: float = 0.0
60
+ self.currentFrame: int = 0
61
+
62
+ self.flashTimer: float = 0.0
63
+ self.flashCounter: int = 0
64
+ self.originalText: bool = True
65
+
66
+ # ------------------------------------------------------------------ #
67
+ # Public API #
68
+ # ------------------------------------------------------------------ #
69
+ def update_dimensions(self, width: int, height: Optional[int] = None) -> None:
70
+ self.canvasDimensions.width = width
71
+ if height is not None:
72
+ self.canvasDimensions.height = height
73
+ # Boyut değiştiğinde animasyonu son kareye ayarla
74
+ self.currentFrame = len(_AnimConfig.frames) - 1
75
+
76
+ def draw(self, alt_game_mode_active: Optional[bool] = None, t_rex: Optional[Any] = None) -> None:
77
+ if alt_game_mode_active is not None:
78
+ self.altGameModeActive = alt_game_mode_active
79
+
80
+ # Metni (orijinal) çiz
81
+ self._draw_game_over_text(_DefaultPanelDims, use_alt_text=False)
82
+ # Restart butonu (mevcut frame)
83
+ self._draw_restart_button()
84
+ # Alt game öğeleri TRex üzerinde
85
+ if t_rex is not None:
86
+ self._draw_alt_game_elements(t_rex)
87
+
88
+ # Zaman ve animasyon adımı
89
+ self._step()
90
+
91
+ def reset(self) -> None:
92
+ self.animTimer = 0.0
93
+ self.frameTimeStamp = 0
94
+ self.currentFrame = 0
95
+ self.flashTimer = 0.0
96
+ self.flashCounter = 0
97
+ self.originalText = True
98
+
99
+ # ------------------------------------------------------------------ #
100
+ # Internal #
101
+ # ------------------------------------------------------------------ #
102
+ def _draw_game_over_text(self, dims, use_alt_text: bool = False) -> None:
103
+ centerX = self.canvasDimensions.width / 2
104
+
105
+ textSourceX = dims.textX
106
+ textSourceY = dims.textY
107
+ textSourceWidth = dims.textWidth
108
+ textSourceHeight = dims.textHeight
109
+
110
+ textTargetX = round(centerX - (dims.textWidth / 2))
111
+ textTargetY = round((self.canvasDimensions.height - 25) / 3)
112
+ textTargetWidth = dims.textWidth
113
+ textTargetHeight = dims.textHeight
114
+
115
+ if IS_HIDPI:
116
+ textSourceY *= 2
117
+ textSourceX *= 2
118
+ textSourceWidth *= 2
119
+ textSourceHeight *= 2
120
+
121
+ if not use_alt_text:
122
+ textSourceX += int(self.textImgPos["x"])
123
+ textSourceY += int(self.textImgPos["y"])
124
+
125
+ spriteSource = (
126
+ self.imageSpriteProvider.getAltCommonImageSprite()
127
+ if use_alt_text
128
+ else self.imageSpriteProvider.getOrigImageSprite()
129
+ )
130
+ assert spriteSource is not None
131
+
132
+ # Kaynaktan bölge
133
+ src_rect = pygame.Rect(textSourceX, textSourceY, textSourceWidth, textSourceHeight)
134
+ frame = spriteSource.subsurface(src_rect).copy()
135
+ if frame.get_size() != (textTargetWidth, textTargetHeight):
136
+ frame = pygame.transform.scale(frame, (textTargetWidth, textTargetHeight))
137
+
138
+ # RTL: ayna konumlandırma
139
+ if IS_RTL:
140
+ dest_x = self.canvasDimensions.width - textTargetX - textTargetWidth
141
+ else:
142
+ dest_x = textTargetX
143
+
144
+ self.canvasCtx.blit(frame, (dest_x, textTargetY))
145
+
146
+ def _draw_alt_game_elements(self, t_rex: Any) -> None:
147
+ spriteDefinition = self.imageSpriteProvider.getSpriteDefinition()
148
+ if self.altGameModeActive and spriteDefinition:
149
+ assert self.altGameEndImgPos is not None
150
+ alt_cfg = spriteDefinition.get("altGameEndConfig")
151
+ assert alt_cfg is not None
152
+
153
+ alt_w = int(alt_cfg["width"])
154
+ alt_h = int(alt_cfg["height"])
155
+ target_x = int(t_rex.xPos + alt_cfg["xOffset"])
156
+ target_y = int(t_rex.yPos + alt_cfg["yOffset"])
157
+
158
+ src_w = alt_w * (2 if IS_HIDPI else 1)
159
+ src_h = alt_h * (2 if IS_HIDPI else 1)
160
+
161
+ altCommon = self.imageSpriteProvider.getAltCommonImageSprite()
162
+ assert altCommon is not None
163
+
164
+ src_rect = pygame.Rect(self.altGameEndImgPos["x"], self.altGameEndImgPos["y"], src_w, src_h)
165
+ frame = altCommon.subsurface(src_rect).copy()
166
+ if frame.get_size() != (alt_w, alt_h):
167
+ frame = pygame.transform.scale(frame, (alt_w, alt_h))
168
+
169
+ # RTL: sadece x'i aynala
170
+ if IS_RTL:
171
+ dest_x = self.canvasDimensions.width - target_x - alt_w
172
+ else:
173
+ dest_x = target_x
174
+
175
+ self.canvasCtx.blit(frame, (dest_x, target_y))
176
+
177
+ def _draw_restart_button(self) -> None:
178
+ dims = _DefaultPanelDims
179
+ # Clamp frame index to avoid IndexError when animation steps past last frame
180
+ frame_index = self.currentFrame
181
+ if frame_index < 0:
182
+ frame_index = 0
183
+ max_idx = len(_AnimConfig.frames) - 1
184
+ if frame_index > max_idx:
185
+ frame_index = max_idx
186
+
187
+ framePosX = _AnimConfig.frames[frame_index]
188
+ src_w = dims.restartWidth
189
+ src_h = dims.restartHeight
190
+
191
+ target_x = int((self.canvasDimensions.width / 2) - (dims.restartWidth / 2))
192
+ target_y = int(self.canvasDimensions.height / 2)
193
+
194
+ if IS_HIDPI:
195
+ src_w *= 2
196
+ src_h *= 2
197
+ framePosX *= 2
198
+
199
+ origSprite = self.imageSpriteProvider.getOrigImageSprite()
200
+ assert origSprite is not None
201
+
202
+ src_rect = pygame.Rect(self.restartImgPos["x"] + framePosX, self.restartImgPos["y"], src_w, src_h)
203
+ frame = origSprite.subsurface(src_rect).copy()
204
+ if frame.get_size() != (dims.restartWidth, dims.restartHeight):
205
+ frame = pygame.transform.scale(frame, (dims.restartWidth, dims.restartHeight))
206
+
207
+ if IS_RTL:
208
+ dest_x = self.canvasDimensions.width - target_x - dims.restartWidth
209
+ else:
210
+ dest_x = target_x
211
+
212
+ self.canvasCtx.blit(frame, (dest_x, target_y))
213
+
214
+ def _clear_game_over_text_bounds(self, dims=_DefaultPanelDims) -> None:
215
+ # TS clearRect → pygame'de beyaz dolduruyoruz
216
+ rect = pygame.Rect(
217
+ round(self.canvasDimensions.width / 2 - (dims.textWidth / 2)),
218
+ round((self.canvasDimensions.height - 25) / 3),
219
+ dims.textWidth,
220
+ dims.textHeight + 4,
221
+ )
222
+ # <--- DÜZELTME: Renk (255, 255, 255) yerine (247, 247, 247) olmalı
223
+ pygame.draw.rect(self.canvasCtx, (247, 247, 247), rect)
224
+
225
+ def _step(self) -> None:
226
+ now = getTimeStamp()
227
+ delta = now - (self.frameTimeStamp or now)
228
+ self.frameTimeStamp = now
229
+ self.animTimer += delta
230
+ self.flashTimer += delta
231
+
232
+ # Restart logosu animasyonu
233
+ if self.currentFrame == 0 and self.animTimer > LOGO_PAUSE_DURATION:
234
+ self.animTimer = 0
235
+ self.currentFrame += 1
236
+ # self._draw_restart_button() # <--- Çizim zaten draw() içinde yapılıyor
237
+ # <--- === DÜZELTME 1: Animasyonun son karede durması için ===
238
+ elif 0 < self.currentFrame < len(_AnimConfig.frames) - 1:
239
+ # =========================================================
240
+ if self.animTimer >= _AnimConfig.msPerFrame:
241
+ self.currentFrame += 1
242
+ # self._draw_restart_button() # <--- Çizim zaten draw() içinde yapılıyor
243
+
244
+ # <--- === DÜZELTME 2: Döngüye neden olan bloğu kaldır ===
245
+ # elif (not self.altGameModeActive) and self.currentFrame == len(_AnimConfig.frames):
246
+ # self.reset()
247
+ # return
248
+ # =====================================================
249
+
250
+ # Game Over yazısı (Alt game metni / flashing)
251
+ altTextCfg = sprite_definition_by_type["original"].get("altGameOverTextConfig")
252
+ if self.altGameModeActive and altTextCfg:
253
+ if altTextCfg.get("flashing"):
254
+ if self.flashCounter < FLASH_ITERATIONS and self.flashTimer > altTextCfg["flashDuration"]:
255
+ self.flashTimer = 0
256
+ self.originalText = not self.originalText
257
+ self._clear_game_over_text_bounds()
258
+ if self.originalText:
259
+ self._draw_game_over_text(_DefaultPanelDims, use_alt_text=False)
260
+ self.flashCounter += 1
261
+ else:
262
+ self._draw_game_over_text(altTextCfg, use_alt_text=True)
263
+ elif self.flashCounter >= FLASH_ITERATIONS:
264
+ # <--- DÜZELTME 3: Döngüyü kaldır, sadece geç ===
265
+ # self.reset()
266
+ # return
267
+ pass
268
+ # ==============================================
269
+ else:
270
+ # flash yoksa, doğrudan alt metni bas
271
+ self._clear_game_over_text_bounds(altTextCfg)
272
+ self._draw_game_over_text(altTextCfg, use_alt_text=True)
pydino/horizon.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # horizon.py
2
+ # Pygame port of horizon.ts (dict/attr uyumlu, 1:1 mantık)
3
+
4
+ from __future__ import annotations
5
+
6
+ import random
7
+ from dataclasses import dataclass
8
+ from typing import Any, Callable, List, Optional
9
+
10
+ import pygame
11
+
12
+ try:
13
+ # Senin utils.py farklı isimde olabilir.
14
+ from utils import get_random_num as getRandomNum
15
+ except Exception:
16
+ from utils import getRandomNum # type: ignore
17
+
18
+ from background_el import (
19
+ BackgroundEl,
20
+ get_global_config as getBackgroundElGlobalConfig,
21
+ set_global_config as setBackgroundElGlobalConfig,
22
+ )
23
+ from cloud import Cloud
24
+ from dimensions import Dimensions
25
+ from horizon_line import HorizonLine
26
+ from image_sprite_provider import ImageSpriteProvider
27
+ from night_mode import NightMode
28
+ from obstacle import (
29
+ Obstacle,
30
+ set_max_gap_coefficient as setMaxObstacleGapCoefficient,
31
+ set_max_obstacle_length as setMaxObstacleLength,
32
+ )
33
+ from offline_sprite_definitions import sprite_definition_by_type
34
+
35
+
36
+ __all__ = ["Horizon"] # dışa açık isim
37
+
38
+ def _get(container: Any, key: str, default: Any = None) -> Any:
39
+ """dict / obj uyumlu erişim."""
40
+ if hasattr(container, key):
41
+ return getattr(container, key)
42
+ if isinstance(container, dict):
43
+ return container.get(key, default)
44
+ return default
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class HorizonConfig:
49
+ BG_CLOUD_SPEED: float
50
+ BUMPY_THRESHOLD: float
51
+ CLOUD_FREQUENCY: float
52
+ HORIZON_HEIGHT: int
53
+ MAX_CLOUDS: int
54
+
55
+
56
+ horizonConfig = HorizonConfig(
57
+ BG_CLOUD_SPEED=0.2,
58
+ BUMPY_THRESHOLD=0.3,
59
+ CLOUD_FREQUENCY=0.5,
60
+ HORIZON_HEIGHT=16,
61
+ MAX_CLOUDS=6,
62
+ )
63
+
64
+
65
+ class Horizon:
66
+ obstacles: List[Obstacle]
67
+
68
+ def __init__(
69
+ self,
70
+ canvas: pygame.Surface,
71
+ spritePos: dict, # SpritePositions (dict)
72
+ dimensions: Dimensions,
73
+ gapCoefficient: float,
74
+ resourceProvider: ImageSpriteProvider, # + getConfig(), hasSlowdown, hasAudioCues
75
+ ) -> None:
76
+ self.obstacles = []
77
+ self.canvas = canvas
78
+ self.canvasCtx = canvas # pygame Surface
79
+ self.config = horizonConfig
80
+ self.dimensions = dimensions
81
+ self.gapCoefficient = gapCoefficient
82
+ self.resourceProvider = resourceProvider
83
+
84
+ self.obstacleHistory: List[str] = []
85
+ self.cloudFrequency = self.config.CLOUD_FREQUENCY
86
+ self.spritePos = spritePos
87
+ self.cloudSpeed = self.config.BG_CLOUD_SPEED
88
+
89
+ self.altGameModeActive: bool = False
90
+ self.obstacleTypes: List[dict] = []
91
+
92
+ # Collections
93
+ self.clouds: List[Cloud] = []
94
+ self.backgroundEls: List[BackgroundEl] = []
95
+ self.lastEl: Optional[str] = None
96
+ self.horizonLines: List[HorizonLine] = []
97
+
98
+ # Başlangıç tanımları
99
+ self.obstacleTypes = list(sprite_definition_by_type["original"]["obstacles"])
100
+ self.addCloud()
101
+
102
+ runnerSpriteDefinition = self.resourceProvider.getSpriteDefinition()
103
+ assert runnerSpriteDefinition is not None
104
+
105
+ for lineDef in runnerSpriteDefinition["lines"]:
106
+ self.horizonLines.append(
107
+ HorizonLine(self.canvas, lineDef, self.resourceProvider)
108
+ )
109
+
110
+ self.nightMode = NightMode(
111
+ self.canvas, _get(self.spritePos, "moon"), self.dimensions.width, self.resourceProvider
112
+ )
113
+
114
+ # -------- helpers --------
115
+ def adjustObstacleSpeed(self) -> None:
116
+ """Slow mode için obstacle tanımlarını ayarla."""
117
+ if not getattr(self.resourceProvider, "hasSlowdown", False):
118
+ return
119
+ for ob in self.obstacleTypes:
120
+ # Güvenli okuma: yoksa değiştirmiyoruz
121
+ if "multipleSpeed" in ob and isinstance(ob["multipleSpeed"], (int, float)):
122
+ ob["multipleSpeed"] = ob["multipleSpeed"] / 2
123
+ if "minGap" in ob and isinstance(ob["minGap"], (int, float)):
124
+ ob["minGap"] = ob["minGap"] * 1.5
125
+ if "minSpeed" in ob and isinstance(ob["minSpeed"], (int, float)):
126
+ ob["minSpeed"] = ob["minSpeed"] / 2
127
+ yPos = ob.get("yPos")
128
+ if isinstance(yPos, list) and len(yPos) > 1:
129
+ ob["yPos"] = yPos[0]
130
+
131
+ def enableAltGameMode(self, spritePos: dict) -> None:
132
+ runnerSpriteDefinition = self.resourceProvider.getSpriteDefinition()
133
+ assert runnerSpriteDefinition is not None
134
+
135
+ # temizle
136
+ self.clouds = []
137
+ self.backgroundEls = []
138
+
139
+ self.altGameModeActive = True
140
+ self.spritePos = spritePos
141
+
142
+ self.obstacleTypes = list(runnerSpriteDefinition["obstacles"])
143
+ self.adjustObstacleSpeed()
144
+
145
+ setMaxObstacleGapCoefficient(runnerSpriteDefinition["maxGapCoefficient"])
146
+ setMaxObstacleLength(runnerSpriteDefinition["maxObstacleLength"])
147
+ setBackgroundElGlobalConfig(runnerSpriteDefinition["backgroundElConfig"])
148
+
149
+ self.horizonLines = []
150
+ for lineDef in runnerSpriteDefinition["lines"]:
151
+ self.horizonLines.append(
152
+ HorizonLine(self.canvas, lineDef, self.resourceProvider)
153
+ )
154
+ self.reset()
155
+
156
+ # -------- frame update --------
157
+ def update(
158
+ self,
159
+ deltaTime: float,
160
+ currentSpeed: float,
161
+ updateObstacles: bool,
162
+ showNightMode: bool,
163
+ ) -> None:
164
+ runnerSpriteDefinition = self.resourceProvider.getSpriteDefinition()
165
+ assert runnerSpriteDefinition is not None
166
+
167
+ if self.altGameModeActive:
168
+ self._updateBackgroundEls(deltaTime)
169
+
170
+ for line in self.horizonLines:
171
+ line.update(deltaTime, currentSpeed)
172
+
173
+ if (not self.altGameModeActive) or runnerSpriteDefinition["hasClouds"]:
174
+ self.nightMode.update(showNightMode)
175
+ self._updateClouds(deltaTime, currentSpeed)
176
+
177
+ if updateObstacles:
178
+ self._updateObstacles(deltaTime, currentSpeed)
179
+
180
+ # -------- background elements / clouds --------
181
+ def _updateBackgroundEl(
182
+ self,
183
+ elSpeed: float,
184
+ bgElArray: List[Cloud | BackgroundEl],
185
+ maxBgEl: int,
186
+ bgElAddFunction: Callable[[], None],
187
+ frequency: float,
188
+ ) -> None:
189
+ numElements = len(bgElArray)
190
+
191
+ if not numElements:
192
+ bgElAddFunction()
193
+ return
194
+
195
+ for i in range(numElements - 1, -1, -1):
196
+ bgElArray[i].update(elSpeed)
197
+
198
+ lastEl = bgElArray[-1]
199
+
200
+ if (
201
+ numElements < maxBgEl
202
+ and (self.dimensions.width - lastEl.xPos) > getattr(lastEl, "gap", 0)
203
+ and frequency > random.random()
204
+ ):
205
+ bgElAddFunction()
206
+
207
+ def _updateClouds(self, deltaTime: float, speed: float) -> None:
208
+ elSpeed = self.cloudSpeed / 1000.0 * deltaTime * speed
209
+ self._updateBackgroundEl(
210
+ elSpeed, self.clouds, self.config.MAX_CLOUDS, self.addCloud, self.cloudFrequency
211
+ )
212
+ # temizle
213
+ self.clouds = [c for c in self.clouds if not c.remove]
214
+
215
+ def _updateBackgroundEls(self, deltaTime: float) -> None:
216
+ self._updateBackgroundEl(
217
+ deltaTime,
218
+ self.backgroundEls,
219
+ _get(getBackgroundElGlobalConfig(), "maxBgEls"),
220
+ self.addBackgroundEl,
221
+ self.cloudFrequency,
222
+ )
223
+ self.backgroundEls = [b for b in self.backgroundEls if not b.remove]
224
+
225
+ # -------- obstacles --------
226
+ def _updateObstacles(self, deltaTime: float, currentSpeed: float) -> None:
227
+ updated = self.obstacles[:]
228
+ for ob in self.obstacles:
229
+ ob.update(deltaTime, currentSpeed)
230
+ if ob.remove and updated:
231
+ updated.pop(0)
232
+ self.obstacles = updated
233
+
234
+ if self.obstacles:
235
+ last = self.obstacles[-1]
236
+ if (
237
+ last
238
+ and not last.followingObstacleCreated
239
+ and last.isVisible()
240
+ and (last.xPos + last.width + last.gap) < self.dimensions.width
241
+ ):
242
+ self.addNewObstacle(currentSpeed)
243
+ last.followingObstacleCreated = True
244
+ else:
245
+ self.addNewObstacle(currentSpeed)
246
+
247
+ def removeFirstObstacle(self) -> None:
248
+ if self.obstacles:
249
+ self.obstacles.pop(0)
250
+
251
+ def addNewObstacle(self, currentSpeed: float) -> None:
252
+ if not self.obstacleTypes:
253
+ return
254
+
255
+ lastType = self.obstacleTypes[-1]
256
+ lastTypeName = _get(lastType, "type")
257
+
258
+ if (
259
+ lastTypeName != "collectable"
260
+ or (
261
+ getattr(self.resourceProvider, "isAltGameModeEnabled", lambda: False)()
262
+ and not self.altGameModeActive
263
+ )
264
+ or self.altGameModeActive
265
+ ):
266
+ obstacleCount = len(self.obstacleTypes) - 1
267
+ else:
268
+ obstacleCount = len(self.obstacleTypes) - 2
269
+
270
+ idx = getRandomNum(0, obstacleCount) if obstacleCount > 0 else 0
271
+ obstacleType = self.obstacleTypes[idx]
272
+ obstacleTypeName = _get(obstacleType, "type")
273
+
274
+ # tekrar & hız kontrol
275
+ if (
276
+ (obstacleCount > 0 and self.duplicateObstacleCheck(obstacleTypeName))
277
+ or currentSpeed < float(_get(obstacleType, "minSpeed", 0))
278
+ ):
279
+ self.addNewObstacle(currentSpeed)
280
+ return
281
+
282
+ obstacleSpritePos = _get(self.spritePos, obstacleTypeName)
283
+
284
+ self.obstacles.append(
285
+ Obstacle(
286
+ self.canvasCtx,
287
+ obstacleType,
288
+ obstacleSpritePos,
289
+ self.dimensions,
290
+ self.gapCoefficient,
291
+ currentSpeed,
292
+ int(_get(obstacleType, "width", 0)),
293
+ self.resourceProvider, # ImageSpriteProvider & GameStateProvider
294
+ self.altGameModeActive,
295
+ )
296
+ )
297
+
298
+ self.obstacleHistory.insert(0, obstacleTypeName)
299
+ if len(self.obstacleHistory) > 1:
300
+ maxDup = _get(self.resourceProvider.getConfig(), "maxObstacleDuplication", 2)
301
+ del self.obstacleHistory[int(maxDup):]
302
+
303
+ def duplicateObstacleCheck(self, nextType: str) -> bool:
304
+ dup = 0
305
+ for t in self.obstacleHistory:
306
+ if t == nextType:
307
+ dup += 1
308
+ else:
309
+ dup = 0
310
+ maxDup = _get(self.resourceProvider.getConfig(), "maxObstacleDuplication", 2)
311
+ return dup >= maxDup
312
+
313
+ # -------- misc --------
314
+ def reset(self) -> None:
315
+ self.obstacles = []
316
+ for line in self.horizonLines:
317
+ line.reset()
318
+ self.nightMode.reset()
319
+
320
+ def resize(self, width: int, height: int) -> None:
321
+ # pygame Surface boyutu display.set_mode ile ayarlanır; burada no-op
322
+ pass
323
+
324
+ def addCloud(self) -> None:
325
+ self.clouds.append(
326
+ Cloud(self.canvas, _get(self.spritePos, "cloud"), self.dimensions.width, self.resourceProvider)
327
+ )
328
+
329
+ def addBackgroundEl(self) -> None:
330
+ runnerSpriteDefinition = self.resourceProvider.getSpriteDefinition()
331
+ assert runnerSpriteDefinition is not None
332
+ backgroundElDict = runnerSpriteDefinition["backgroundEl"]
333
+ backgroundElTypes = list(backgroundElDict.keys())
334
+ if not backgroundElTypes:
335
+ return
336
+
337
+ index = getRandomNum(0, len(backgroundElTypes) - 1)
338
+ elType = backgroundElTypes[index]
339
+ while elType == self.lastEl and len(backgroundElTypes) > 1:
340
+ index = getRandomNum(0, len(backgroundElTypes) - 1)
341
+ elType = backgroundElTypes[index]
342
+
343
+ self.lastEl = elType
344
+ self.backgroundEls.append(
345
+ BackgroundEl(
346
+ self.canvas,
347
+ _get(self.spritePos, "backgroundEl"),
348
+ self.dimensions.width,
349
+ elType,
350
+ self.resourceProvider,
351
+ )
352
+ )
pydino/horizon_line.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # horizon_line.py
2
+ # Pygame port of horizon_line.ts (dict/attr uyumlu)
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Dict, Tuple
7
+ import pygame
8
+
9
+ try:
10
+ from constants import FPS, IS_HIDPI
11
+ except ImportError:
12
+ from .constants import FPS, IS_HIDPI
13
+ from image_sprite_provider import ImageSpriteProvider
14
+
15
+ SpritePosition = Dict[str, int] # {"x": int, "y": int}
16
+
17
+ def _get(container: Any, key: str, default=None):
18
+ if isinstance(container, dict):
19
+ return container.get(key, default)
20
+ return getattr(container, key, default)
21
+
22
+ class HorizonLine:
23
+ def __init__(
24
+ self,
25
+ canvas: pygame.Surface,
26
+ line_config: Dict[str, int] | Any,
27
+ image_sprite_provider: ImageSpriteProvider,
28
+ ) -> None:
29
+ # Kaynak konumları (sprite sheet içindeki yatay 2 parça)
30
+ source_x = _get(line_config, "sourceX")
31
+ source_y = _get(line_config, "sourceY")
32
+ width = _get(line_config, "width")
33
+ height = _get(line_config, "height")
34
+ self.yPos = _get(line_config, "yPos")
35
+
36
+ if source_x is None or source_y is None or width is None or height is None:
37
+ raise ValueError("HorizonLine: line_config eksik alan içeriyor.")
38
+
39
+ if IS_HIDPI:
40
+ source_x *= 2
41
+ source_y *= 2
42
+
43
+ # Canvas & provider
44
+ self.canvas = canvas
45
+ self.canvasCtx = canvas # pygame Surface
46
+ self.imageSpriteProvider = image_sprite_provider
47
+
48
+ # Sprite sheet üzerindeki başlangıç x’leri
49
+ self.spritePos: SpritePosition = {"x": source_x, "y": source_y}
50
+ self.dimensions: Tuple[int, int] = (width, height)
51
+
52
+ # İki parça halinde zemin çizgisi
53
+ self.sourceXPos = [self.spritePos["x"], self.spritePos["x"] + width]
54
+ self.xPos = [0, width]
55
+
56
+ # Kesilecek kaynak boyutları (HiDPI’de 2x)
57
+ self.sourceDimensions = {
58
+ "width": width * (2 if IS_HIDPI else 1),
59
+ "height": height * (2 if IS_HIDPI else 1),
60
+ }
61
+
62
+ self.bumpThreshold = 0.5 # bumpy/flat geçiş olasılığı
63
+ self.draw()
64
+
65
+ def getRandomType(self) -> int:
66
+ """Bumpy/flat seçimi: ikinci parçanın kaynak offset’i."""
67
+ return self.dimensions[0] if (pygame.time.get_ticks() % 1000) / 1000.0 > self.bumpThreshold else 0
68
+
69
+ def draw(self) -> None:
70
+ runner_image = self.image_sprite_provider_image()
71
+ sw = self.sourceDimensions["width"]
72
+ sh = self.sourceDimensions["height"]
73
+ dw, dh = self.dimensions
74
+
75
+ # parça 1
76
+ self.canvasCtx.blit(
77
+ runner_image,
78
+ (self.xPos[0], self.yPos),
79
+ (self.sourceXPos[0], self.spritePos["y"], sw, sh),
80
+ )
81
+ # parça 2
82
+ self.canvasCtx.blit(
83
+ runner_image,
84
+ (self.xPos[1], self.yPos),
85
+ (self.sourceXPos[1], self.spritePos["y"], sw, sh),
86
+ )
87
+
88
+ def updatexPos(self, pos: int, increment: int) -> None:
89
+ line1 = pos
90
+ line2 = 1 - pos
91
+
92
+ self.xPos[line1] -= increment
93
+ self.xPos[line2] = self.xPos[line1] + self.dimensions[0]
94
+
95
+ if self.xPos[line1] <= -self.dimensions[0]:
96
+ self.xPos[line1] += self.dimensions[0] * 2
97
+ self.xPos[line2] = self.xPos[line1] - self.dimensions[0]
98
+ # bumpy/flat seçimi: ikinci “kaynak x” başlangıcı
99
+ self.sourceXPos[line1] = self.getRandomType() + self.spritePos["x"]
100
+
101
+ def update(self, deltaTime: float, speed: float) -> None:
102
+ increment = int(speed * (FPS / 1000.0) * deltaTime)
103
+ self.updatexPos(0 if self.xPos[0] <= 0 else 1, increment)
104
+ self.draw()
105
+
106
+ def reset(self) -> None:
107
+ self.xPos[0] = 0
108
+ self.xPos[1] = self.dimensions[0]
109
+
110
+ # ---- helpers ----
111
+ def image_sprite_provider_image(self) -> pygame.Surface:
112
+ img = self.imageSpriteProvider.getRunnerImageSprite()
113
+ assert img is not None
114
+ return img
pydino/image_sprite_provider.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # image_sprite_provider.py
2
+ # 1:1 pygame port of image_sprite_provider.ts (interface + a simple impl)
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Optional, Protocol
7
+
8
+ import pygame
9
+
10
+ from offline_sprite_definitions import SpriteDefinition, sprite_definition_by_type
11
+
12
+
13
+ class ImageSpriteProvider(Protocol):
14
+ """Dino oyununda paylaşılan sprite sheet'lere erişim arayüzü."""
15
+
16
+ def getOrigImageSprite(self) -> pygame.Surface: ...
17
+ def getRunnerImageSprite(self) -> pygame.Surface: ...
18
+ def getRunnerAltGameImageSprite(self) -> Optional[pygame.Surface]: ...
19
+ def getAltCommonImageSprite(self) -> Optional[pygame.Surface]: ...
20
+ def getSpriteDefinition(self) -> SpriteDefinition: ...
21
+
22
+
23
+ def _load_image(path: str) -> pygame.Surface:
24
+ """Load image with guaranteed alpha preservation, even before display init."""
25
+ if not pygame.get_init():
26
+ pygame.init()
27
+ if not pygame.display.get_init():
28
+ pygame.display.init()
29
+ pygame.display.set_mode((1, 1))
30
+ surf = pygame.image.load(path)
31
+ if surf.get_alpha() is not None:
32
+ surf = surf.convert_alpha()
33
+ else:
34
+ surf = surf.convert()
35
+ surf.set_colorkey((0, 0, 0))
36
+ return surf
37
+
38
+
39
+ def _get_original_sprite_definition() -> SpriteDefinition:
40
+ """TS tarafında spriteDefinitionByType.original vardı;
41
+ Python port'unda dict/attr her iki olasılığı da destekle."""
42
+ try:
43
+ return sprite_definition_by_type["original"] # dict tarzı
44
+ except Exception:
45
+ return getattr(sprite_definition_by_type, "original") # attr tarzı
46
+
47
+
48
+ class PygameImageSpriteProvider(ImageSpriteProvider):
49
+ """Basit bir sağlayıcı: tek bir sprite sheet kullanır (orig=runner).
50
+ Alt oyun görselleri opsiyoneldir (None döner)."""
51
+
52
+ def __init__(
53
+ self,
54
+ sprite_path: str,
55
+ *,
56
+ alt_game_path: Optional[str] = None,
57
+ alt_common_path: Optional[str] = None,
58
+ ) -> None:
59
+ self._orig = _load_image(sprite_path)
60
+ # Chrome dino'da orig ve runner aynı sheet olabilir; bire bir yapıyoruz.
61
+ self._runner = self._orig
62
+ self._alt_game = _load_image(alt_game_path) if alt_game_path else None
63
+ self._alt_common = _load_image(alt_common_path) if alt_common_path else None
64
+ self._sprite_def = _get_original_sprite_definition()
65
+
66
+ # --- ImageSpriteProvider implementation ---
67
+ def getOrigImageSprite(self) -> pygame.Surface:
68
+ return self._orig
69
+
70
+ def getRunnerImageSprite(self) -> pygame.Surface:
71
+ return self._runner
72
+
73
+ def getRunnerAltGameImageSprite(self) -> Optional[pygame.Surface]:
74
+ return self._alt_game
75
+
76
+ def getAltCommonImageSprite(self) -> Optional[pygame.Surface]:
77
+ return self._alt_common
78
+
79
+ def getSpriteDefinition(self) -> SpriteDefinition:
80
+ return self._sprite_def
pydino/night_mode.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # night_mode.py
2
+ # Pygame port of night_mode.ts (dict/attr uyumlu)
3
+
4
+ from __future__ import annotations
5
+ from typing import Any, Dict, List, TypedDict
6
+ import pygame
7
+
8
+ try:
9
+ from constants import IS_HIDPI
10
+ except ImportError:
11
+ from .constants import IS_HIDPI
12
+ from offline_sprite_definitions import sprite_definition_by_type
13
+ from utils import get_random_num
14
+
15
+ # Ay fazlarının spritesheet içindeki x-pozisyonları (ldpi referansı)
16
+ PHASES: List[int] = [140, 120, 100, 60, 40, 20, 0]
17
+
18
+ class _Cfg:
19
+ FADE_SPEED = 0.035
20
+ HEIGHT = 40
21
+ MOON_SPEED = 0.25
22
+ NUM_STARS = 2
23
+ STAR_SIZE = 9
24
+ STAR_SPEED = 0.3
25
+ STAR_MAX_Y = 70
26
+ WIDTH = 20
27
+
28
+ class NightMode:
29
+ def __init__(
30
+ self,
31
+ canvas: pygame.Surface,
32
+ sprite_pos: Dict[str, int], # {"x": int, "y": int}
33
+ container_width: int,
34
+ image_sprite_provider
35
+ ) -> None:
36
+ self.canvas = canvas
37
+ self.canvasCtx = canvas
38
+ self.sprite_pos = sprite_pos
39
+ self.provider = image_sprite_provider
40
+
41
+ self.container_width = container_width
42
+
43
+ self.xPos: float = 0.0
44
+ self.yPos: float = 30.0
45
+ self.currentPhase: int = 0
46
+ self.opacity: float = 0.0
47
+ self.drawStars: bool = False
48
+
49
+ # yıldızlar: x,y, sourceY
50
+ self.stars: List[Dict[str, float]] = [dict(x=0.0, y=0.0, sourceY=0.0) for _ in range(_Cfg.NUM_STARS)]
51
+
52
+ self._place_stars()
53
+
54
+ def update(self, activated: bool) -> None:
55
+ # Ay fazı geçişi yalnızca görünür olmaya başlarken değişsin
56
+ if activated and self.opacity == 0:
57
+ self.currentPhase += 1
58
+ if self.currentPhase >= len(PHASES):
59
+ self.currentPhase = 0
60
+
61
+ # Fade in / out
62
+ if activated and (self.opacity < 1 or self.opacity == 0):
63
+ self.opacity += _Cfg.FADE_SPEED
64
+ if self.opacity > 1:
65
+ self.opacity = 1
66
+ elif self.opacity > 0:
67
+ self.opacity -= _Cfg.FADE_SPEED
68
+ if self.opacity < 0:
69
+ self.opacity = 0
70
+
71
+ # Pozisyon güncelle / çiz
72
+ if self.opacity > 0:
73
+ self.xPos = self._update_xpos(self.xPos, _Cfg.MOON_SPEED)
74
+ if self.drawStars:
75
+ for s in self.stars:
76
+ s["x"] = self._update_xpos(float(s["x"]), _Cfg.STAR_SPEED)
77
+ self._draw()
78
+ else:
79
+ # Görünür değilken yıldızları yeniden yerleştir
80
+ self._place_stars()
81
+
82
+ self.drawStars = True
83
+
84
+ def reset(self) -> None:
85
+ self.currentPhase = 0
86
+ self.opacity = 0
87
+ self.update(False)
88
+
89
+ # ---- iç yardımcılar ----
90
+ def _update_xpos(self, current: float, speed: float) -> float:
91
+ if current < -_Cfg.WIDTH:
92
+ return float(self.container_width)
93
+ return current - speed
94
+
95
+ def _draw(self) -> None:
96
+ # Ay kaynak dikdörtgeni
97
+ moon_src_w = _Cfg.WIDTH * (2 if self.currentPhase == 3 else 1)
98
+ moon_src_h = _Cfg.HEIGHT
99
+ phase_offset = PHASES[self.currentPhase]
100
+ moon_src_x = self.sprite_pos["x"] + phase_offset
101
+ moon_src_y = self.sprite_pos["y"]
102
+
103
+ # HiDPI düzeltmeleri (spritesheet 2x ise kaynak boyutları 2x)
104
+ star_src_size = _Cfg.STAR_SIZE
105
+ star_sheet = sprite_definition_by_type["original"]["ldpi"]["star"]
106
+ if IS_HIDPI:
107
+ moon_src_w *= 2
108
+ moon_src_h *= 2
109
+ moon_src_x = self.sprite_pos["x"] + (phase_offset * 2)
110
+ star_src_size *= 2
111
+ star_sheet = sprite_definition_by_type["original"]["hdpi"]["star"]
112
+
113
+ # Kaynak görsel (orijinal sheet)
114
+ runner_orig = self.provider.getOrigImageSprite()
115
+ assert runner_orig is not None
116
+
117
+ # --- YENI: opacity'den alpha hesapla ve fade'i gerçekten uygula ---
118
+ alpha = int(max(0, min(255, self.opacity * 255)))
119
+ if alpha <= 0:
120
+ return # görünmezken çizme
121
+
122
+ # Yıldızlar
123
+ for s in self.stars:
124
+ src_rect = pygame.Rect(star_sheet["x"], int(s["sourceY"]), star_src_size, star_src_size)
125
+ # subsurface -> copy -> alpha uygula
126
+ star_tile = runner_orig.subsurface(src_rect).copy()
127
+ star_tile.set_alpha(alpha)
128
+ self.canvasCtx.blit(star_tile, (int(s["x"]), int(s["y"])))
129
+
130
+ # Ay
131
+ moon_src_rect = pygame.Rect(moon_src_x, moon_src_y, moon_src_w, moon_src_h)
132
+ moon_tile = runner_orig.subsurface(moon_src_rect).copy()
133
+ moon_tile.set_alpha(alpha)
134
+ self.canvasCtx.blit(moon_tile, (int(self.xPos), int(self.yPos)))
135
+
136
+ def _place_stars(self) -> None:
137
+ # ldpi/hdpi y bazları
138
+ if IS_HIDPI:
139
+ star_y_base = sprite_definition_by_type["original"]["hdpi"]["star"]["y"]
140
+ step = _Cfg.STAR_SIZE * 2
141
+ else:
142
+ star_y_base = sprite_definition_by_type["original"]["ldpi"]["star"]["y"]
143
+ step = _Cfg.STAR_SIZE
144
+
145
+ segment = max(1, round(self.container_width / _Cfg.NUM_STARS))
146
+ for i in range(_Cfg.NUM_STARS):
147
+ self.stars[i]["x"] = float(get_random_num(segment * i, segment * (i + 1)))
148
+ self.stars[i]["y"] = float(get_random_num(0, _Cfg.STAR_MAX_Y))
149
+ self.stars[i]["sourceY"] = float(star_y_base + step * i)
pydino/obstacle.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # obstacle.py
2
+ # 1:1 pygame port of obstacle.ts
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Any, Optional
8
+
9
+ import pygame
10
+
11
+ try:
12
+ from constants import FPS, IS_HIDPI, IS_MOBILE
13
+ from utils import get_random_num
14
+ except ImportError:
15
+ from .constants import FPS, IS_HIDPI, IS_MOBILE
16
+ from .utils import get_random_num
17
+ from offline_sprite_definitions import CollisionBox # your py version
18
+
19
+
20
+ # -----------------------------------------------------------------------------
21
+ # Global tunables (match TS module-level state)
22
+ # -----------------------------------------------------------------------------
23
+ max_gap_coefficient: float = 1.5
24
+ max_obstacle_length: int = 3
25
+
26
+
27
+ def set_max_gap_coefficient(coefficient: float) -> None:
28
+ global max_gap_coefficient
29
+ max_gap_coefficient = coefficient
30
+
31
+
32
+ def set_max_obstacle_length(length: int) -> None:
33
+ global max_obstacle_length
34
+ max_obstacle_length = int(length)
35
+
36
+
37
+ # TS-compatible aliases (so imports like setMaxGapCoefficient work)
38
+ setMaxGapCoefficient = set_max_gap_coefficient
39
+ setMaxObstacleLength = set_max_obstacle_length
40
+
41
+
42
+ # -----------------------------------------------------------------------------
43
+ # Helpers
44
+ # -----------------------------------------------------------------------------
45
+ def _get(obj: Any, name: str, default: Any = None) -> Any:
46
+ """Support both dataclass/attr objects and dict-like configs."""
47
+ if hasattr(obj, name):
48
+ return getattr(obj, name)
49
+ if isinstance(obj, dict):
50
+ return obj.get(name, default)
51
+ return default
52
+
53
+
54
+ def _blit_region(
55
+ dst_surface: pygame.Surface,
56
+ src_surface: pygame.Surface,
57
+ src_rect: pygame.Rect,
58
+ dest_xy: tuple[int, int],
59
+ dest_wh: tuple[int, int],
60
+ ) -> None:
61
+ sub = src_surface.subsurface(src_rect)
62
+ if sub.get_width() != dest_wh[0] or sub.get_height() != dest_wh[1]:
63
+ sub = pygame.transform.scale(sub, dest_wh)
64
+ dst_surface.blit(sub, dest_xy)
65
+
66
+
67
+ # -----------------------------------------------------------------------------
68
+ # Obstacle
69
+ # -----------------------------------------------------------------------------
70
+ class Obstacle:
71
+ # Public fields (match TS names exactly)
72
+ collisionBoxes: list[CollisionBox]
73
+ followingObstacleCreated: bool
74
+ gap: int
75
+ jumpAlerted: bool
76
+ remove: bool
77
+ size: int
78
+ width: int
79
+ xPos: int
80
+ yPos: int
81
+ typeConfig: Any # ObstacleType in TS
82
+
83
+ # private-ish
84
+ _canvas: pygame.Surface
85
+ _sprite_pos: Any # SpritePosition (expects .x, .y)
86
+ _gap_coefficient: float
87
+ _speed_offset: float
88
+ _alt_game_mode_active: bool
89
+ _image_sprite: pygame.Surface
90
+ _current_frame: int
91
+ _timer: float
92
+ _resource_provider: Any # ImageSpriteProvider & GameStateProvider
93
+
94
+ def __init__(
95
+ self,
96
+ canvas_ctx: pygame.Surface,
97
+ type_config: Any, # ObstacleType (dataclass/dict)
98
+ sprite_img_pos: Any, # SpritePosition (.x, .y)
99
+ dimensions: Any, # Dimensions (width, height)
100
+ gap_coefficient: float,
101
+ speed: float,
102
+ x_offset: float = 0,
103
+ resource_provider: Any = None, # provides image sprites & state flags
104
+ is_alt_game_mode: bool = False,
105
+ ) -> None:
106
+ self._canvas = canvas_ctx
107
+ self._sprite_pos = sprite_img_pos
108
+ self.typeConfig = type_config
109
+ self._resource_provider = resource_provider
110
+
111
+ has_slowdown = bool(getattr(resource_provider, "hasSlowdown", False))
112
+ self._gap_coefficient = gap_coefficient * 2 if has_slowdown else gap_coefficient
113
+
114
+ self.size = get_random_num(1, max_obstacle_length)
115
+ self.xPos = int(_get(dimensions, "width", 600) + x_offset)
116
+ self.yPos = 0
117
+
118
+ self.followingObstacleCreated = False
119
+ self.gap = 0
120
+ self.jumpAlerted = False
121
+ self.remove = False
122
+ self.width = 0
123
+
124
+ self._alt_game_mode_active = bool(is_alt_game_mode)
125
+ self._speed_offset = 0.0
126
+ self._current_frame = 0
127
+ self._timer = 0.0
128
+
129
+ # Pick correct sprite sheet surface
130
+ img_surface: Optional[pygame.Surface]
131
+ t_type = _get(self.typeConfig, "type")
132
+ if t_type == "collectable":
133
+ img_surface = getattr(resource_provider, "getAltCommonImageSprite")()
134
+ elif self._alt_game_mode_active:
135
+ img_surface = getattr(resource_provider, "getRunnerAltGameImageSprite")()
136
+ else:
137
+ img_surface = getattr(resource_provider, "getRunnerImageSprite")()
138
+ assert img_surface is not None, "Image sprite surface could not be loaded."
139
+ self._image_sprite = img_surface
140
+
141
+ # Finish setup
142
+ self._init(speed)
143
+
144
+ # -------------------------------------------------------------------------
145
+ # TS private init()
146
+ # -------------------------------------------------------------------------
147
+ def _init(self, speed: float) -> None:
148
+ self.cloneCollisionBoxes()
149
+
150
+ # Only allow sizing if at the right speed.
151
+ multiple_speed = float(_get(self.typeConfig, "multipleSpeed", 9999))
152
+ if self.size > 1 and multiple_speed > speed:
153
+ self.size = 1
154
+
155
+ base_width = int(_get(self.typeConfig, "width", 0))
156
+ base_height = int(_get(self.typeConfig, "height", 0))
157
+ self.width = base_width * self.size
158
+
159
+ # yPos can be an int or a list (with optional yPosMobile)
160
+ y_pos = _get(self.typeConfig, "yPos")
161
+ if isinstance(y_pos, (list, tuple)):
162
+ y_mobile = _get(self.typeConfig, "yPosMobile", y_pos)
163
+ selectable = y_mobile if IS_MOBILE else y_pos
164
+ idx = get_random_num(0, len(selectable) - 1)
165
+ self.yPos = int(selectable[idx])
166
+ else:
167
+ self.yPos = int(y_pos)
168
+
169
+ # Initial draw
170
+ self._draw()
171
+
172
+ # Adjust collision boxes for size > 1
173
+ if self.size > 1 and len(self.collisionBoxes) >= 3:
174
+ # center box width spans total width minus side boxes
175
+ left = self.collisionBoxes[0]
176
+ center = self.collisionBoxes[1]
177
+ right = self.collisionBoxes[2]
178
+ center.width = self.width - left.width - right.width
179
+ right.x = self.width - right.width
180
+
181
+ # speedOffset
182
+ speed_offset = _get(self.typeConfig, "speedOffset")
183
+ if speed_offset:
184
+ self._speed_offset = float(speed_offset if (get_random_num(0, 1) == 1) else -speed_offset)
185
+
186
+ # gap
187
+ self.gap = self.getGap(self._gap_coefficient, speed)
188
+
189
+ # Increase gap if audio cues enabled
190
+ if bool(getattr(self._resource_provider, "hasAudioCues", False)):
191
+ self.gap *= 2
192
+
193
+ # -------------------------------------------------------------------------
194
+ # TS private draw()
195
+ # -------------------------------------------------------------------------
196
+ def _draw(self) -> None:
197
+ source_w = int(_get(self.typeConfig, "width", 0))
198
+ source_h = int(_get(self.typeConfig, "height", 0))
199
+
200
+ # HIDPI ise kaynaktan 2x alan oku
201
+ src_w_px = source_w * (2 if IS_HIDPI else 1)
202
+ src_h_px = source_h * (2 if IS_HIDPI else 1)
203
+
204
+ # Sprite sheet üzerindeki başlangıç X konumu:
205
+ # (sourceWidth * size) * (0.5 * (size - 1)) + spritePos.x
206
+ base = (src_w_px * self.size) * (0.5 * (self.size - 1))
207
+
208
+ # sprite_pos hem dict hem obje olabilir — güvenli oku
209
+ pos_x = int(_get(self._sprite_pos, "x", 0))
210
+ pos_y = int(_get(self._sprite_pos, "y", 0))
211
+
212
+ src_x = int(base + pos_x)
213
+ src_y = pos_y
214
+
215
+ # Animasyon çerçevesi ofseti
216
+ if self._current_frame > 0:
217
+ src_x += src_w_px * self._current_frame
218
+
219
+ # Kaynak dikdörtgen ve hedef boyut (mantıksal ölçekte)
220
+ src_rect = pygame.Rect(src_x, src_y, src_w_px * self.size, src_h_px)
221
+ dest_wh = (source_w * self.size, source_h)
222
+
223
+ _blit_region(
224
+ self._canvas,
225
+ self._image_sprite,
226
+ src_rect,
227
+ (int(self.xPos), int(self.yPos)),
228
+ dest_wh,
229
+ )
230
+
231
+ # -------------------------------------------------------------------------
232
+ # TS update(deltaTime, speed)
233
+ # -------------------------------------------------------------------------
234
+ def update(self, delta_time: float, speed: float) -> None:
235
+ if self.remove:
236
+ return
237
+
238
+ # Per-obstacle speed offset
239
+ if _get(self.typeConfig, "speedOffset"):
240
+ speed = float(speed) + self._speed_offset
241
+
242
+ # Move left
243
+ self.xPos -= int((speed * (FPS / 1000.0)) * delta_time)
244
+
245
+ # Animated obstacles
246
+ num_frames = _get(self.typeConfig, "numFrames")
247
+ if num_frames:
248
+ frame_rate = float(_get(self.typeConfig, "frameRate", 0))
249
+ self._timer += delta_time
250
+ if self._timer >= frame_rate:
251
+ self._current_frame = 0 if self._current_frame >= int(num_frames) - 1 else self._current_frame + 1
252
+ self._timer = 0.0
253
+
254
+ # Draw current frame
255
+ self._draw()
256
+
257
+ # Cull when off-screen
258
+ if not self.isVisible():
259
+ self.remove = True
260
+
261
+ # -------------------------------------------------------------------------
262
+ # TS getGap(...)
263
+ # -------------------------------------------------------------------------
264
+ def getGap(self, gap_coefficient: float, speed: float) -> int:
265
+ min_gap = int(round(self.width * speed + _get(self.typeConfig, "minGap", 0) * gap_coefficient))
266
+ max_gap = int(round(min_gap * max_gap_coefficient))
267
+ return get_random_num(min_gap, max_gap)
268
+
269
+ # -------------------------------------------------------------------------
270
+ # TS isVisible()
271
+ # -------------------------------------------------------------------------
272
+ def isVisible(self) -> bool:
273
+ return (self.xPos + self.width) > 0
274
+
275
+ # Pythonic alias
276
+ def is_visible(self) -> bool:
277
+ return self.isVisible()
278
+
279
+ # -------------------------------------------------------------------------
280
+ # TS cloneCollisionBoxes()
281
+ # -------------------------------------------------------------------------
282
+ def cloneCollisionBoxes(self) -> None:
283
+ self.collisionBoxes = []
284
+ boxes = _get(self.typeConfig, "collisionBoxes", []) or []
285
+ for b in boxes:
286
+ # b may be dict or CollisionBox-like
287
+ self.collisionBoxes.append(
288
+ CollisionBox(
289
+ x=int(_get(b, "x", 0)),
290
+ y=int(_get(b, "y", 0)),
291
+ width=int(_get(b, "width", 0)),
292
+ height=int(_get(b, "height", 0)),
293
+ )
294
+ )
pydino/offline_sprite_definitions.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # offline_sprite_definitions.py
2
+ # 1:1 port of Chrome Dino offline_sprite_definitions.ts for pygame.
3
+
4
+ from __future__ import annotations
5
+ from dataclasses import dataclass
6
+ from typing import Dict, List, Optional, Union, TypedDict, Any
7
+
8
+ # --- helpers to allow both dict["x"] and obj.x access ----------------------
9
+ class _DotAccessDict(dict):
10
+ def __getattr__(self, key):
11
+ try:
12
+ return self[key]
13
+ except KeyError:
14
+ raise AttributeError(key)
15
+
16
+ def __setattr__(self, key, value):
17
+ if key in ("__setstate__",):
18
+ return super().__setattr__(key, value)
19
+ self[key] = value
20
+
21
+ def __delattr__(self, key):
22
+ try:
23
+ del self[key]
24
+ except KeyError:
25
+ raise AttributeError(key)
26
+
27
+ class SpritePos(_DotAccessDict):
28
+ def __init__(self, x: int, y: int):
29
+ super().__init__({"x": int(x), "y": int(y)})
30
+
31
+ class LineConf(_DotAccessDict):
32
+ def __init__(self, sourceX: int, sourceY: int, width: int, height: int, yPos: int):
33
+ super().__init__({
34
+ "sourceX": int(sourceX),
35
+ "sourceY": int(sourceY),
36
+ "width": int(width),
37
+ "height": int(height),
38
+ "yPos": int(yPos),
39
+ })
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Types (lightweight Python equivalents)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ @dataclass
46
+ class CollisionBox:
47
+ x: int
48
+ y: int
49
+ width: int
50
+ height: int
51
+
52
+
53
+ @dataclass
54
+ class ObstacleType:
55
+ # Keys mirror TS interface
56
+ type: str
57
+ width: int
58
+ height: int
59
+ yPos: Union[int, List[int]]
60
+ multipleSpeed: float
61
+ minGap: int
62
+ minSpeed: float
63
+ collisionBoxes: List[CollisionBox]
64
+ # optional
65
+ yPosMobile: Optional[List[int]] = None
66
+ speedOffset: Optional[float] = None
67
+ numFrames: Optional[int] = None
68
+ frameRate: Optional[float] = None
69
+
70
+
71
+ class SpritePosition(TypedDict):
72
+ x: int
73
+ y: int
74
+
75
+
76
+ # SpritePositions in TS is an object with many sprite anchors.
77
+ SpritePositions = Dict[str, SpritePosition]
78
+
79
+ # Loosely typed containers for convenience (dict-like)
80
+ SpriteDefinition = Dict[str, Any]
81
+ SpriteDefinitionByType = Dict[str, SpriteDefinition]
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # GAME_TYPE list (TS exports an empty list; keep parity)
85
+ # ---------------------------------------------------------------------------
86
+ GAME_TYPE: List[str] = []
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Sprite definitions (values copied 1:1 from TS)
90
+ # ---------------------------------------------------------------------------
91
+
92
+ _ldpi_positions: SpritePositions = {
93
+ "backgroundEl": {"x": 86, "y": 2},
94
+ "cactusLarge": {"x": 332, "y": 2},
95
+ "cactusSmall": {"x": 228, "y": 2},
96
+ "obstacle2": {"x": 332, "y": 2},
97
+ "obstacle": {"x": 228, "y": 2},
98
+ "cloud": {"x": 86, "y": 2},
99
+ "horizon": {"x": 2, "y": 54},
100
+ "moon": {"x": 484, "y": 2},
101
+ "pterodactyl": {"x": 134, "y": 2},
102
+ "restart": {"x": 2, "y": 68},
103
+ "textSprite": {"x": 655, "y": 2},
104
+ "tRex": {"x": 848, "y": 2},
105
+ "star": {"x": 645, "y": 2},
106
+ "collectable": {"x": 0, "y": 0},
107
+ "altGameEnd": {"x": 32, "y": 0},
108
+ }
109
+
110
+ _hdpi_positions: SpritePositions = {
111
+ "backgroundEl": {"x": 166, "y": 2},
112
+ "cactusLarge": {"x": 652, "y": 2},
113
+ "cactusSmall": {"x": 446, "y": 2},
114
+ "obstacle2": {"x": 652, "y": 2},
115
+ "obstacle": {"x": 446, "y": 2},
116
+ "cloud": {"x": 166, "y": 2},
117
+ "horizon": {"x": 2, "y": 104},
118
+ "moon": {"x": 954, "y": 2},
119
+ "pterodactyl": {"x": 260, "y": 2},
120
+ "restart": {"x": 2, "y": 130},
121
+ "textSprite": {"x": 1294, "y": 2},
122
+ "tRex": {"x": 1678, "y": 2},
123
+ "star": {"x": 1276, "y": 2},
124
+ "collectable": {"x": 0, "y": 0},
125
+ "altGameEnd": {"x": 64, "y": 0},
126
+ }
127
+
128
+ # Wrap sprite positions to support both ["x"] and .x usage
129
+ _ldpi_positions_wrapped: SpritePositions = {k: SpritePos(v["x"], v["y"]) for k, v in _ldpi_positions.items()}
130
+ _hdpi_positions_wrapped: SpritePositions = {k: SpritePos(v["x"], v["y"]) for k, v in _hdpi_positions.items()}
131
+
132
+ # Obstacles (dataclass instances for attribute access)
133
+ _obstacles: List[ObstacleType] = [
134
+ ObstacleType(
135
+ type="cactusSmall",
136
+ width=17,
137
+ height=35,
138
+ yPos=105,
139
+ multipleSpeed=4,
140
+ minGap=120,
141
+ minSpeed=0,
142
+ collisionBoxes=[
143
+ CollisionBox(x=0, y=7, width=5, height=27),
144
+ CollisionBox(x=4, y=0, width=6, height=34),
145
+ CollisionBox(x=10, y=4, width=7, height=14),
146
+ ],
147
+ ),
148
+ ObstacleType(
149
+ type="cactusLarge",
150
+ width=25,
151
+ height=50,
152
+ yPos=90,
153
+ multipleSpeed=7,
154
+ minGap=120,
155
+ minSpeed=0,
156
+ collisionBoxes=[
157
+ CollisionBox(x=0, y=12, width=7, height=38),
158
+ CollisionBox(x=8, y=0, width=7, height=49),
159
+ CollisionBox(x=13, y=10, width=10, height=38),
160
+ ],
161
+ ),
162
+ ObstacleType(
163
+ type="pterodactyl",
164
+ width=46,
165
+ height=40,
166
+ yPos=[100, 75, 50], # variable heights
167
+ yPosMobile=[100, 50],
168
+ multipleSpeed=999,
169
+ minSpeed=8.5,
170
+ minGap=150,
171
+ collisionBoxes=[
172
+ CollisionBox(x=15, y=15, width=16, height=5),
173
+ CollisionBox(x=18, y=21, width=24, height=6),
174
+ CollisionBox(x=2, y=14, width=4, height=3),
175
+ CollisionBox(x=6, y=10, width=4, height=7),
176
+ CollisionBox(x=10, y=8, width=6, height=9),
177
+ ],
178
+ numFrames=2,
179
+ frameRate=1000.0 / 6.0,
180
+ speedOffset=0.8,
181
+ ),
182
+ ObstacleType(
183
+ type="collectable",
184
+ width=31,
185
+ height=24,
186
+ yPos=104,
187
+ multipleSpeed=1000,
188
+ minGap=9999,
189
+ minSpeed=0,
190
+ collisionBoxes=[
191
+ CollisionBox(x=0, y=0, width=32, height=25),
192
+ ],
193
+ ),
194
+ ]
195
+
196
+ _background_el: Dict[str, Dict[str, Union[int, float, bool]]] = {
197
+ "CLOUD": {
198
+ "height": 14,
199
+ "offset": 4,
200
+ "width": 46,
201
+ "xPos": 1,
202
+ "fixed": False,
203
+ },
204
+ }
205
+
206
+ _background_el_config: Dict[str, Union[int, float]] = {
207
+ "maxBgEls": 1,
208
+ "maxGap": 400,
209
+ "minGap": 100,
210
+ "pos": 0,
211
+ "speed": 0.5,
212
+ "yPos": 125,
213
+ }
214
+
215
+ _lines: List[Dict[str, int]] = [
216
+ {"sourceX": 2, "sourceY": 52, "width": 600, "height": 12, "yPos": 127},
217
+ ]
218
+
219
+ _lines_wrapped = [LineConf(**line) for line in _lines]
220
+
221
+ _alt_game_over_text_config: Dict[str, Union[int, float, bool]] = {
222
+ "textX": 32,
223
+ "textY": 0,
224
+ "textWidth": 246,
225
+ "textHeight": 17,
226
+ "flashDuration": 1500,
227
+ "flashing": False,
228
+ }
229
+
230
+ sprite_definition_by_type: SpriteDefinitionByType = {
231
+ "original": {
232
+ "ldpi": _ldpi_positions_wrapped,
233
+ "hdpi": _hdpi_positions_wrapped,
234
+ "maxGapCoefficient": 1.5,
235
+ "maxObstacleLength": 3,
236
+ "hasClouds": True,
237
+ "bottomPad": 10,
238
+ "obstacles": _obstacles,
239
+ "backgroundEl": _background_el,
240
+ "backgroundElConfig": _background_el_config,
241
+ "lines": _lines_wrapped,
242
+ "altGameOverTextConfig": _alt_game_over_text_config,
243
+ # "altGameEndConfig": ... # optional, not used in original
244
+ }
245
+ }
pydino/runner.py ADDED
@@ -0,0 +1,766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import math
3
+ import os
4
+ import pygame
5
+ from dataclasses import dataclass
6
+
7
+ from typing import Dict, Optional, List, Any
8
+
9
+ # Debug overlay (optional)
10
+ try:
11
+ from debug_overlay import CollisionDebugOverlay
12
+ except Exception:
13
+ CollisionDebugOverlay = None # overlay is optional; keep game running if missing
14
+
15
+ # NumPy import'unu en başta yapıyoruz
16
+ try:
17
+ import numpy as np
18
+ import pygame.surfarray as sarr
19
+ NUMPY_AVAILABLE = True
20
+ except ImportError:
21
+ NUMPY_AVAILABLE = False
22
+
23
+ # ==== USER PATHS (dynamic) ==================================================
24
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
25
+ ASSETS_DIR = os.path.join(BASE_DIR, "assets")
26
+ SOUNDS_DIR = os.path.join(BASE_DIR, "sounds")
27
+
28
+ SPRITE_PATH = os.path.join(ASSETS_DIR, "100-offline-sprite.png")
29
+ SND_BUTTON = os.path.join(SOUNDS_DIR, "button-press.mp3")
30
+ SND_HIT = os.path.join(SOUNDS_DIR, "hit.mp3")
31
+ SND_SCORE = os.path.join(SOUNDS_DIR, "score-reached.mp3")
32
+ # ============================================================================
33
+
34
+ # ==== Imports from your ported modules ======================================
35
+ # ==== Imports from your ported modules ======================================
36
+ try:
37
+ from constants import FPS
38
+ from dimensions import Dimensions
39
+ from horizon import Horizon
40
+ from distance_meter import DistanceMeter
41
+ from game_over_panel import GameOverPanel
42
+ from trex import Trex, Status as TrexStatus
43
+ from offline_sprite_definitions import (
44
+ sprite_definition_by_type,
45
+ CollisionBox, GAME_TYPE
46
+ )
47
+ except ImportError:
48
+ from .constants import FPS
49
+ from .dimensions import Dimensions
50
+ from .horizon import Horizon
51
+ from .distance_meter import DistanceMeter
52
+ from .game_over_panel import GameOverPanel
53
+ from .trex import Trex, Status as TrexStatus
54
+ from .offline_sprite_definitions import (
55
+ sprite_definition_by_type,
56
+ CollisionBox, GAME_TYPE
57
+ )
58
+ print("Lütfen tüm .py dosyalarının aynı dizinde olduğundan emin olun.")
59
+ raise SystemExit(1)
60
+
61
+ # ==== Configs (merged from defaultBaseConfig + normalModeConfig) =============
62
+ @dataclass
63
+ class Config:
64
+ # defaultBaseConfig
65
+ audiocueProximityThreshold: int = 190
66
+ audiocueProximityThresholdMobileA11y: int = 250
67
+ bgCloudSpeed: float = 0.2
68
+ bottomPad: int = 10
69
+ canvasInViewOffset: int = -10
70
+ clearTime: int = 3000
71
+ cloudFrequency: float = 0.5
72
+ fadeDuration: float = 1
73
+ flashDuration: int = 1000
74
+ gameoverClearTime: int = 1200
75
+ initialJumpVelocity: float = 12
76
+ invertFadeDuration: int = 12000
77
+ maxBlinkCount: int = 3
78
+ maxClouds: int = 6
79
+ maxObstacleLength: int = 3
80
+ maxObstacleDuplication: int = 2
81
+ resourceTemplateId: str = "audio-resources"
82
+ speedDropCoefficient: float = 3
83
+ arcadeModeInitialTopPosition: int = 35
84
+ arcadeModeTopPositionPercent: float = 0.1
85
+ # normalModeConfig
86
+ acceleration: float = 0.001
87
+ gapCoefficient: float = 0.6
88
+ invertDistance: int = 700
89
+ maxSpeed: float = 13.0
90
+ mobileSpeedCoefficient: float = 1.2
91
+ speed: float = 6.0
92
+
93
+
94
+ # ==== Utility (canvas scaling / collisions) ==================================
95
+ def create_adjusted_box(box: CollisionBox, adj: CollisionBox) -> CollisionBox:
96
+ return CollisionBox(box.x + adj.x, box.y + adj.y, box.width, box.height)
97
+
98
+ def box_intersect(a: CollisionBox, b: CollisionBox) -> bool:
99
+ return (
100
+ a.x < b.x + b.width and
101
+ a.x + a.width > b.x and
102
+ a.y < b.y + b.height and
103
+ a.height + a.y > b.y
104
+ )
105
+
106
+ def _get(obj, name, default=None):
107
+ """dict / obje uyumlu alan okuma"""
108
+ if hasattr(obj, name):
109
+ return getattr(obj, name)
110
+ if isinstance(obj, dict):
111
+ return obj.get(name, default)
112
+ return default
113
+
114
+
115
+ # ==== Runner =================================================================
116
+ class Runner:
117
+ """
118
+ Pygame orchestrator matching Chrome Dino Runner logic.
119
+ Provides ImageSpriteProvider, GameStateProvider, ConfigProvider interfaces.
120
+ """
121
+ # Key map
122
+ KEY_JUMP = (pygame.K_UP, pygame.K_SPACE)
123
+ KEY_DUCK = (pygame.K_DOWN,)
124
+ KEY_RESTART = (pygame.K_RETURN,)
125
+
126
+ # Yeni sabitler
127
+ NIGHT_ALPHA_MAX = 180
128
+ VISUAL_INVERT_MS = 1000
129
+
130
+ def _get_visual_invert_ms(self) -> int:
131
+ ms = self.VISUAL_INVERT_MS
132
+ if self._invert_mode_pixels:
133
+ ms = int(ms * 0.75)
134
+ return max(120, ms)
135
+
136
+ def _ease_in_out_cubic(self, t: float) -> float:
137
+ """CSS-like ease-in-out (cubic) for more natural fade perception."""
138
+ if t <= 0.0:
139
+ return 0.0
140
+ if t >= 1.0:
141
+ return 1.0
142
+ if t < 0.5:
143
+ return 4.0 * t * t * t
144
+ return 1.0 - ((-2.0 * t + 2.0) ** 3) / 2.0
145
+
146
+ def __init__(self, screen: pygame.Surface, dimensions: Dimensions,
147
+ use_audio: bool = True) -> None:
148
+ self.screen = screen
149
+ self.dimensions = dimensions
150
+ self.config = Config()
151
+ self.ms_per_frame = 1000.0 / FPS
152
+
153
+ # provider state
154
+ self._has_slowdown = False
155
+ self._has_audio_cues = use_audio
156
+ self._alt_game_mode_active = False
157
+ self._alt_game_assets_failed = False
158
+ self._game_type: Optional[str] = None
159
+
160
+ # spritesheet (LDPI by default)
161
+ self.sprite_image: Optional[pygame.Surface] = None
162
+ self.sprite_alt_game_image: Optional[pygame.Surface] = None
163
+ self.sprite_alt_common_image: Optional[pygame.Surface] = None
164
+
165
+ # sprite positions (ldpi)
166
+ self.sprite_def = sprite_definition_by_type["original"]["ldpi"]
167
+
168
+ # Components
169
+ self.horizon: Optional[Horizon] = None
170
+ self.distance_meter: Optional[DistanceMeter] = None
171
+ self.trex: Optional[Trex] = None
172
+ self.game_over_panel: Optional[GameOverPanel] = None
173
+
174
+ # Game flow flags/state
175
+ self.activated = False
176
+ self.playing = False
177
+ self.playing_intro = False
178
+ self.crashed = False
179
+ self.paused = False
180
+ self.inverted = False
181
+ self.is_dark_mode = False
182
+ self.update_pending = False
183
+
184
+ # Intro zamanlayıcı
185
+ self.intro_start_time = 0
186
+ self.INTRO_DURATION = 400 # 400ms
187
+ self.intro_start_width = 44
188
+ # Intro sadece ilk açılışta oynasın
189
+ self.did_intro = False
190
+
191
+ # Gece modu overlay yüzeyi ve bayrakları
192
+ self._night_overlay = pygame.Surface((self.dimensions.width, self.dimensions.height), pygame.SRCALPHA)
193
+ self._invert_mode_pixels = NUMPY_AVAILABLE
194
+ # Debug overlay instance (may be None if import failed)
195
+ self.debug = CollisionDebugOverlay() if CollisionDebugOverlay else None
196
+
197
+ # Timers & numeric state
198
+ self.time_ms = pygame.time.get_ticks()
199
+ self.distance_ran = 0.0
200
+ self.running_time = 0.0
201
+ self.current_speed = self.config.speed
202
+ self.invert_timer = 0.0
203
+ self.invert_trigger = False
204
+ self.fade_in_timer = 0.0
205
+ self.alt_game_mode_flash_timer: Optional[float] = None
206
+ # Game over bekleme süresi için damga
207
+ self.game_over_time: Optional[int] = None
208
+
209
+ # Night-mode state machine
210
+ self._inv_phase = "day"
211
+ self._inv_progress = 0.0
212
+ self._next_invert_score = self.config.invertDistance
213
+
214
+ # High score
215
+ self.highest_score = 0
216
+ self.sync_highest_score = False
217
+
218
+ # Sounds
219
+ self.snd_button = None
220
+ self.snd_hit = None
221
+ self.snd_score = None
222
+ if self._has_audio_cues:
223
+ self._load_sounds()
224
+
225
+ # Load images & init components
226
+ self._load_images()
227
+ self._init_components()
228
+
229
+ # Intro başlangıç genişliği (Trex oluştuktan sonra)
230
+ assert self.trex is not None
231
+ trex_width = _get(self.trex.config, "width", 44)
232
+ self.intro_start_width = self.trex.xPos + trex_width
233
+
234
+ # ========== Provider interfaces ==========
235
+ @property
236
+ def hasSlowdown(self) -> bool:
237
+ return self._has_slowdown
238
+
239
+ @property
240
+ def hasAudioCues(self) -> bool:
241
+ return self._has_audio_cues
242
+
243
+ def isAltGameModeEnabled(self) -> bool:
244
+ return False if self._alt_game_assets_failed else False
245
+
246
+ def getSpriteDefinition(self) -> Dict[str, Any]:
247
+ return sprite_definition_by_type["original"]
248
+
249
+ def getOrigImageSprite(self) -> pygame.Surface:
250
+ assert self.sprite_image is not None
251
+ return self.sprite_image
252
+
253
+ def getRunnerImageSprite(self) -> pygame.Surface:
254
+ assert self.sprite_image is not None
255
+ return self.sprite_image
256
+
257
+ def getRunnerAltGameImageSprite(self) -> Optional[pygame.Surface]:
258
+ return self.sprite_alt_game_image
259
+
260
+ def getAltCommonImageSprite(self) -> Optional[pygame.Surface]:
261
+ return self.sprite_alt_common_image
262
+
263
+ def getConfig(self) -> Config:
264
+ return self.config
265
+
266
+ # ========== Asset loading ==========
267
+ def _load_images(self) -> None:
268
+ try:
269
+ img = pygame.image.load(SPRITE_PATH).convert_alpha()
270
+ self.sprite_image = img
271
+ self.sprite_def = sprite_definition_by_type["original"]["ldpi"]
272
+ except pygame.error as e:
273
+ print(f"HATA: Sprite dosyası yüklenemedi: {SPRITE_PATH}")
274
+ print(f"Detay: {e}")
275
+ raise e
276
+
277
+ def _load_sounds(self) -> None:
278
+ try:
279
+ # Daha stabil/düşük gecikme ve yeterli kanal
280
+ pygame.mixer.pre_init(44100, -16, 2, 512)
281
+ pygame.mixer.init()
282
+ pygame.mixer.set_num_channels(8)
283
+
284
+ def _try_load_sound(path: str) -> pygame.mixer.Sound:
285
+ try:
286
+ return pygame.mixer.Sound(path)
287
+ except Exception:
288
+ # Yedek uzantıları dene (.wav/.ogg)
289
+ base, _ = os.path.splitext(path)
290
+ for ext in ('.wav', '.ogg'):
291
+ alt = base + ext
292
+ if os.path.exists(alt):
293
+ try:
294
+ return pygame.mixer.Sound(alt)
295
+ except Exception:
296
+ continue
297
+ raise
298
+
299
+ self.snd_button = _try_load_sound(SND_BUTTON)
300
+ self.snd_hit = _try_load_sound(SND_HIT)
301
+ self.snd_score = _try_load_sound(SND_SCORE)
302
+
303
+ # Varsayılan ses seviyeleri
304
+ try:
305
+ self.snd_button.set_volume(0.6)
306
+ self.snd_hit.set_volume(0.8)
307
+ self.snd_score.set_volume(0.7)
308
+ except Exception:
309
+ pass
310
+
311
+ self._has_audio_cues = True
312
+ except Exception:
313
+ self._has_audio_cues = False
314
+ self.snd_button = self.snd_hit = self.snd_score = None
315
+ print("Uyarı: Ses dosyaları yüklenemedi, sesler devre dışı bırakıldı.")
316
+
317
+ # ========== Init components ==========
318
+ def _init_components(self) -> None:
319
+ self.screen.fill((247, 247, 247))
320
+ self.horizon = Horizon(
321
+ self.screen, self.sprite_def, self.dimensions,
322
+ self.config.gapCoefficient, self
323
+ )
324
+ self.distance_meter = DistanceMeter(
325
+ self.screen, self.sprite_def["textSprite"],
326
+ self.dimensions.width, self
327
+ )
328
+ self.trex = Trex(self.screen, self.sprite_def["tRex"], self)
329
+
330
+ # ========== Public controls ==========
331
+ def start(self) -> None:
332
+ self.playing = True
333
+ self.paused = False
334
+
335
+ def stop(self) -> None:
336
+ self.playing = False
337
+ self.paused = True
338
+
339
+ def restart(self) -> None:
340
+ assert self.horizon and self.trex and self.distance_meter
341
+ self.playing = True
342
+ self.paused = False
343
+ self.crashed = False
344
+ # Restart'ta intro yok; direkt aktif başla
345
+ self.activated = True
346
+ self.playing_intro = False
347
+ if self.trex:
348
+ self.trex.playing_intro = False
349
+ self.distance_ran = 0.0
350
+ self.running_time = 0.0
351
+ self.current_speed = self.config.speed
352
+ self.time_ms = pygame.time.get_ticks()
353
+ self.horizon.reset()
354
+ self.trex.reset()
355
+ self.distance_meter.reset()
356
+ self._play_sound(self.snd_button)
357
+
358
+ # Game over panel reset
359
+ if self.game_over_panel:
360
+ self.game_over_panel.reset()
361
+
362
+ # Game-over zamanı sıfırla
363
+ self.game_over_time = None
364
+
365
+ self.invert(reset=True)
366
+
367
+ # ========== Event handling ==========
368
+ def handle_event(self, e: pygame.event.Event) -> None:
369
+ if e.type == pygame.KEYDOWN:
370
+ # CRASH SONRASI:
371
+ # Enter -> anında restart
372
+ # Space/Up -> 1200 ms bekleme kuralı
373
+ if self.crashed:
374
+ if e.key in self.KEY_RESTART: # Enter
375
+ self._on_restart(force=True)
376
+ return
377
+ if e.key in self.KEY_JUMP: # Space veya Up
378
+ self._on_restart(force=False)
379
+ return
380
+ if e.key in self.KEY_JUMP:
381
+ self._on_jump_down()
382
+ elif e.key in self.KEY_DUCK:
383
+ self._on_duck_down()
384
+ elif e.key == pygame.K_F3 and self.debug is not None:
385
+ # Toggle collision debug visualization
386
+ self.debug.enabled = not self.debug.enabled
387
+
388
+ elif e.type == pygame.KEYUP:
389
+ if e.key in self.KEY_JUMP:
390
+ self._on_jump_up()
391
+ elif e.key in self.KEY_DUCK:
392
+ self._on_duck_up()
393
+ elif e.type == pygame.WINDOWFOCUSLOST:
394
+ pass
395
+
396
+ def _on_jump_down(self) -> None:
397
+ assert self.trex
398
+ if not self.playing and not self.crashed:
399
+ self.start()
400
+ # Havada değil ve eğilmiyorken: zıpla + ses
401
+ if not self.trex.jumping and not self.trex.ducking:
402
+ self._play_sound(self.snd_button)
403
+ self.trex.startJump(self.current_speed)
404
+
405
+ def _on_duck_down(self) -> None:
406
+ assert self.trex
407
+ if self.playing:
408
+ if self.trex.jumping:
409
+ self.trex.setSpeedDrop()
410
+ elif not self.trex.ducking:
411
+ self.trex.setDuck(True)
412
+
413
+ def _on_jump_up(self) -> None:
414
+ assert self.trex
415
+ if self.playing:
416
+ self.trex.endJump()
417
+ elif self.crashed:
418
+ self._on_restart()
419
+
420
+ def _on_duck_up(self) -> None:
421
+ assert self.trex
422
+ self.trex.speedDrop = False
423
+ self.trex.setDuck(False)
424
+
425
+ def _on_restart(self, force: bool = False) -> None:
426
+ # force=True ise (Enter), beklemeden anında başlat
427
+ if force:
428
+ self.restart()
429
+ return
430
+
431
+ # Space/Up için: gameoverClearTime (1200 ms) bekleme kuralı
432
+ now = pygame.time.get_ticks()
433
+ t0 = self.game_over_time if self.game_over_time is not None else self.time_ms
434
+ if now - t0 >= self.config.gameoverClearTime:
435
+ self.restart()
436
+
437
+ # ========== Update loop ==========
438
+ def update(self) -> None:
439
+ """Call this once per frame from your game loop."""
440
+ assert self.trex and self.horizon and self.distance_meter
441
+ now = pygame.time.get_ticks()
442
+ delta = now - self.time_ms
443
+ self.time_ms = now
444
+
445
+ # Alt game flash timer
446
+ if self.alt_game_mode_flash_timer is not None:
447
+ if self.alt_game_mode_flash_timer <= 0:
448
+ self.alt_game_mode_flash_timer = None
449
+ self.trex.setFlashing(False)
450
+ else:
451
+ self.alt_game_mode_flash_timer -= delta
452
+ self.trex.update(delta)
453
+ delta = 0
454
+
455
+ # 1. EKRANI TEMİZLE
456
+ self.screen.fill((247, 247, 247))
457
+
458
+ # 2. GÜNCELLEMELER (FİZİK/ZAMANLAMA)
459
+ if self.playing and self.trex.jumping:
460
+ self.trex.updateJump(delta)
461
+
462
+ if self.playing:
463
+ self.running_time += delta
464
+
465
+ has_obstacles = self.running_time > self.config.clearTime
466
+
467
+ clip_rect = None # Varsayılan klip
468
+ horizon_delta = delta
469
+ horizon_speed = self.current_speed
470
+ show_night_mode = (self.is_dark_mode != self.inverted)
471
+
472
+ # 3. INTRO DURUM KONTROLÜ VE KLİP HESAPLAMA
473
+ # Intro sadece ilk açılışta, ilk zıplamada tetiklensin
474
+ if (not self.did_intro) and self.trex.jumpCount == 1 and not self.playing_intro and not self.activated:
475
+ self.playing_intro = True
476
+ self.trex.playing_intro = True
477
+ self.intro_start_time = pygame.time.get_ticks()
478
+ self.activated = True
479
+
480
+ if not self.activated:
481
+ tx = int(getattr(self.trex, "xPos", 0))
482
+ tw = int(_get(self.trex.config, "width", 44))
483
+ ideal = tx + tw
484
+ if ideal > self.intro_start_width:
485
+ self.intro_start_width = min(self.dimensions.width, ideal)
486
+
487
+ clip_rect = pygame.Rect(0, 0, self.intro_start_width, self.dimensions.height)
488
+ horizon_delta = 0
489
+ show_night_mode = False
490
+
491
+ elif self.playing_intro:
492
+ elapsed_time = pygame.time.get_ticks() - self.intro_start_time
493
+ if elapsed_time >= self.INTRO_DURATION:
494
+ self.playing_intro = False
495
+ self.trex.playing_intro = False
496
+ self.running_time = 0
497
+ self.did_intro = True # bir kez oynatıldı
498
+ else:
499
+ progress = self._ease_in_out_cubic(elapsed_time / self.INTRO_DURATION)
500
+ end_width = self.dimensions.width
501
+ start_width = self.intro_start_width
502
+ current_width = start_width + (end_width - start_width) * progress
503
+ clip_rect = pygame.Rect(0, 0, int(current_width), self.dimensions.height)
504
+ horizon_delta = 0
505
+ show_night_mode = False
506
+
507
+ elif self.crashed:
508
+ horizon_delta = 0 # Zemin durmalı
509
+
510
+ # 4. KLİP AYARLAMA
511
+ self.screen.set_clip(clip_rect) # None ise tüm ekranı kullanır
512
+
513
+ # 5. KLİPLİ ÇİZİM
514
+ # Crashed olsa bile horizon'ı son durumuyla çiz (donmuş arka plan)
515
+ if (self.playing and not self.crashed) or not self.activated or self.crashed:
516
+ self.horizon.update(horizon_delta, horizon_speed, has_obstacles, show_night_mode)
517
+
518
+ # DistanceMeter (sadece oynarken güncelle)
519
+ play_achievement = False
520
+ if self.playing:
521
+ play_achievement = self.distance_meter.update(delta, math.ceil(self.distance_ran))
522
+ elif self.crashed:
523
+ # Kaza anında skor tablosunu ekranda tut
524
+ self.distance_meter.update(0, math.ceil(self.distance_ran))
525
+
526
+ # Trex
527
+ self.trex.update(delta)
528
+ if not self.playing and not self.activated and hasattr(self.trex, "draw"):
529
+ self.trex.draw(0, 0)
530
+
531
+ # 6. KLİP KALDIR
532
+ self.screen.set_clip(None)
533
+
534
+ # 7. OYUN MANTIĞI
535
+ if self.playing and not self.playing_intro and not self.crashed:
536
+ first_obstacle = self.horizon.obstacles[0] if self.horizon.obstacles else None
537
+ collision = False
538
+ if has_obstacles and first_obstacle:
539
+ collision_result = self._check_for_collision(first_obstacle, self.trex)
540
+ if collision_result:
541
+ collision = True
542
+
543
+ if not collision:
544
+ self.distance_ran += self.current_speed * (delta / self.ms_per_frame)
545
+ if self.current_speed < self.config.maxSpeed:
546
+ self.current_speed += self.config.acceleration
547
+ else:
548
+ self._game_over()
549
+
550
+ if play_achievement and self._has_audio_cues:
551
+ self._play_sound(self.snd_score)
552
+
553
+ # Gece modu
554
+ actual = self.distance_meter.getActualDistance(math.ceil(self.distance_ran))
555
+ if not self.isAltGameModeEnabled():
556
+ if self.invert_timer > self.config.invertFadeDuration:
557
+ self.invert_timer = 0
558
+ self.invert_trigger = False
559
+ self.invert(reset=False)
560
+ elif self.invert_timer:
561
+ self.invert_timer += delta
562
+ else:
563
+ if actual > 0 and (actual % self.config.invertDistance == 0) and not self.invert_trigger:
564
+ self.invert_timer += delta
565
+ self.invert(reset=False)
566
+ self.invert_trigger = True
567
+ elif (actual % self.config.invertDistance) != 0:
568
+ self.invert_trigger = False
569
+
570
+ T = self.config.invertFadeDuration
571
+ t = self.invert_timer
572
+ vis = self._get_visual_invert_ms()
573
+
574
+ inv_factor = 0.0
575
+ if self.inverted:
576
+ if t == 0:
577
+ inv_factor = 1.0
578
+ elif t < vis:
579
+ inv_factor = self._ease_in_out_cubic(t / vis)
580
+ elif t <= (T - vis):
581
+ inv_factor = 1.0
582
+ else:
583
+ inv_factor = self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis))
584
+ else:
585
+ if t > 0:
586
+ if t > (T - vis):
587
+ inv_factor = self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis))
588
+
589
+ # 8. OVERLAY
590
+ if inv_factor > 0:
591
+ if self._invert_mode_pixels:
592
+ self._apply_invert_pixels(inv_factor)
593
+ else:
594
+ self._apply_night_overlay_fallback(inv_factor)
595
+
596
+ # 9. GAME OVER PANELİ
597
+ if self.crashed and self.game_over_panel:
598
+ self.game_over_panel.draw(self._alt_game_mode_active, self.trex)
599
+
600
+ # 10. DEBUG OVERLAY (draw last so it sits on top)
601
+ if self.debug is not None and self.debug.enabled:
602
+ self.debug.draw(self.screen, self.trex, self.horizon.obstacles if self.horizon else [])
603
+
604
+ # ========== Game over ==========
605
+ def _game_over(self) -> None:
606
+ assert self.distance_meter and self.trex
607
+ self._play_sound(self.snd_hit)
608
+ self.stop()
609
+ self.crashed = True
610
+ self.distance_meter.achievement = False
611
+ self.trex.update(100, TrexStatus.CRASHED)
612
+ if self.game_over_panel is None:
613
+ orig_def = sprite_definition_by_type["original"]["ldpi"]
614
+ self.game_over_panel = GameOverPanel(
615
+ self.screen, orig_def["textSprite"], orig_def["restart"],
616
+ self.dimensions, self
617
+ )
618
+
619
+ if self.distance_ran > self.highest_score:
620
+ self.highest_score = int(math.ceil(self.distance_ran))
621
+ self.distance_meter.setHighScore(self.highest_score)
622
+
623
+ # Skoru/HI'ı çöküş karesinde de çizer (delta=0 ile donmuş)
624
+ self.distance_meter.update(0, math.ceil(self.distance_ran))
625
+
626
+ # Game over anını damgala (restart bekleme için)
627
+ self.game_over_time = pygame.time.get_ticks()
628
+ self.time_ms = self.game_over_time
629
+
630
+ # ========== Helpers ==========
631
+ def invert(self, reset: bool) -> None:
632
+ if reset:
633
+ self.invert_timer = 0
634
+ self.inverted = False
635
+ else:
636
+ self.inverted = not self.inverted
637
+
638
+ def _get_invert_fade_factor(self) -> float:
639
+ if self.invert_timer == 0:
640
+ return 0.0 if not self.inverted else 1.0
641
+
642
+ current_factor = min(1.0, self.invert_timer / self.config.invertFadeDuration)
643
+ return current_factor if self.inverted else (1.0 - current_factor)
644
+
645
+ def _apply_invert_pixels(self, fade_factor: float):
646
+ if fade_factor <= 0.0:
647
+ return
648
+ try:
649
+ src = sarr.pixels3d(self.screen).copy().astype(np.float32)
650
+ out = src * (1.0 - fade_factor) + (255.0 - src) * fade_factor
651
+ np.rint(out, out=out)
652
+ sarr.blit_array(self.screen, out.astype(np.uint8))
653
+ except Exception:
654
+ self._apply_night_overlay_fallback(fade_factor)
655
+
656
+ def _apply_night_overlay_fallback(self, fade_factor: float):
657
+ a = int(self.NIGHT_ALPHA_MAX * fade_factor)
658
+ if a > 0:
659
+ self._night_overlay.fill((0, 0, 0, a))
660
+ self.screen.blit(self._night_overlay, (0, 0))
661
+
662
+ def _play_sound(self, snd: Optional[pygame.mixer.Sound]) -> None:
663
+ if snd is not None and self._has_audio_cues:
664
+ try:
665
+ ch = pygame.mixer.find_channel(True) # boş kanal bul, yoksa yarat
666
+ if ch is not None:
667
+ ch.play(snd)
668
+ else:
669
+ snd.play()
670
+ except Exception:
671
+ pass
672
+
673
+ # Collision port
674
+ def _check_for_collision(self, obstacle, trex: Trex) -> Optional[List[CollisionBox]]:
675
+ t_width = _get(trex.config, "width")
676
+ t_height = _get(trex.config, "height")
677
+ tbox = CollisionBox(
678
+ trex.xPos + 1,
679
+ trex.yPos + 1,
680
+ int(t_width) - 2,
681
+ int(t_height) - 2,
682
+ )
683
+
684
+ o_width = _get(obstacle.typeConfig, "width")
685
+ o_height = _get(obstacle.typeConfig, "height")
686
+ obox = CollisionBox(
687
+ obstacle.xPos + 1,
688
+ obstacle.yPos + 1,
689
+ int(o_width) * obstacle.size - 2,
690
+ int(o_height) - 2,
691
+ )
692
+
693
+ if not (
694
+ tbox.x < obox.x + obox.width and
695
+ tbox.x + tbox.width > obox.x and
696
+ tbox.y < obox.y + obox.height and
697
+ tbox.height + tbox.y > obox.y
698
+ ):
699
+ return None
700
+
701
+ if self.isAltGameModeEnabled():
702
+ runner_sprite_def = self.getSpriteDefinition()
703
+ tboxes = _get(_get(runner_sprite_def, "tRex", {}), "collisionBoxes", []) or []
704
+ norm = []
705
+ for b in tboxes:
706
+ if isinstance(b, CollisionBox):
707
+ norm.append(b)
708
+ else:
709
+ norm.append(
710
+ CollisionBox(
711
+ int(_get(b, "x", 0)),
712
+ int(_get(b, "y", 0)),
713
+ int(_get(b, "width", 0)),
714
+ int(_get(b, "height", 0)),
715
+ )
716
+ )
717
+ tboxes = norm
718
+ else:
719
+ tboxes = trex.getCollisionBoxes()
720
+
721
+ for tb in tboxes:
722
+ for ob in obstacle.collisionBoxes:
723
+ adj_t = create_adjusted_box(tb, tbox)
724
+ adj_o = create_adjusted_box(ob, obox)
725
+ if (
726
+ adj_t.x < adj_o.x + adj_o.width and
727
+ adj_t.x + adj_t.width > adj_o.x and
728
+ adj_t.y < adj_o.y + adj_o.height and
729
+ adj_t.height + adj_o.y > adj_o.y
730
+ ):
731
+ return [adj_t, adj_o]
732
+ return None
733
+
734
+
735
+ # ==== Optional standalone run for smoke test =================
736
+ if __name__ == "__main__":
737
+ # Ses için mixer’i init’ten önce yapılandır
738
+ pygame.mixer.pre_init(44100, -16, 2, 512)
739
+ pygame.init()
740
+ pygame.display.set_caption("NeuroDino — Runner (pygame)")
741
+ dims = Dimensions(width=600, height=150)
742
+ screen = pygame.display.set_mode((dims.width, dims.height))
743
+ clock = pygame.time.Clock()
744
+
745
+ try:
746
+ runner = Runner(screen, dims, use_audio=True)
747
+ except Exception as e:
748
+ print(f"Oyun başlatılırken kritik bir hata oluştu: {e}")
749
+ pygame.quit()
750
+ raise SystemExit(1)
751
+
752
+ running = True
753
+ while running:
754
+ clock.tick(FPS)
755
+
756
+ for event in pygame.event.get():
757
+ if event.type == pygame.QUIT:
758
+ running = False
759
+ if event.type == pygame.WINDOWFOCUSLOST:
760
+ continue
761
+ runner.handle_event(event)
762
+
763
+ runner.update()
764
+ pygame.display.flip()
765
+
766
+ pygame.quit()
pydino/sounds/button-press.mp3 ADDED
Binary file (5.18 kB). View file
 
pydino/sounds/hit.mp3 ADDED
Binary file (7.21 kB). View file
 
pydino/sounds/score-reached.mp3 ADDED
Binary file (9.51 kB). View file
 
pydino/trex.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # trex.py
2
+ # pygame port of Chrome Dino trex.ts (config fixed to avoid dataclass issues)
3
+
4
+ from __future__ import annotations
5
+ from typing import Dict, List, Optional, Any
6
+ import random # Göz kırpma için random import edildi
7
+
8
+ import pygame
9
+
10
+ def _blit_region_alpha(dst: pygame.Surface,
11
+ src: pygame.Surface,
12
+ src_rect: pygame.Rect,
13
+ dest_xy: tuple[int, int],
14
+ dest_wh: tuple[int, int] | None = None,
15
+ overall_alpha: int | None = None) -> None:
16
+ """
17
+ Sprite sheet'ten bir dikdörtgeni per‑pixel alpha veya colorkey ile çizer.
18
+ Colorkey ve convert() kullanarak siyah arka planı kaldırır.
19
+ """
20
+ try:
21
+ # Alt yüzey al, colorkey uygula ve convert et.
22
+ frame = src.subsurface(src_rect).copy()
23
+ frame.set_colorkey((0, 0, 0)) # Siyah rengi transparan anahtar olarak ayarla
24
+ frame = frame.convert() # Convert (alpha değil) çağırarak colorkey'i etkinleştir.
25
+
26
+ if dest_wh is not None and (frame.get_width(), frame.get_height()) != dest_wh:
27
+ # Ölçeklemede alpha kullanılmadığı için scale kullanmak daha iyi olabilir
28
+ frame = pygame.transform.scale(frame, dest_wh)
29
+ # Ölçekleme sonrası yeni yüzeye colorkey ayarını tekrar uygulayın
30
+ frame.set_colorkey((0, 0, 0))
31
+
32
+ # Alpha ayarı (Colorkey kullanıldığı için bu satırlar büyük ihtimalle etkisiz kalacaktır)
33
+ if overall_alpha is not None:
34
+ frame.set_alpha(overall_alpha)
35
+ else:
36
+ frame.set_alpha(None) # Alpha'yı kaldırır (varsayılan opaklık)
37
+
38
+ dst.blit(frame, dest_xy)
39
+ except ValueError:
40
+ # Subsurface koordinatları hatalıysa görmezden gel (geçici çözüm)
41
+ # print(f"Uyarı: Geçersiz subsurface rect: {src_rect}")
42
+ pass
43
+
44
+
45
+ try:
46
+ from constants import DEFAULT_DIMENSIONS, FPS
47
+ from offline_sprite_definitions import CollisionBox
48
+ except ImportError:
49
+ from .constants import DEFAULT_DIMENSIONS, FPS
50
+ from .offline_sprite_definitions import CollisionBox
51
+
52
+ # utils.get_time_stamp -> getTimeStamp fallback to pygame time
53
+ try:
54
+ from utils import get_time_stamp as getTimeStamp
55
+ except Exception: # fallback
56
+ def getTimeStamp() -> int:
57
+ return pygame.time.get_ticks()
58
+
59
+
60
+ # ---------------- Config helper (dict + attribute access) ------------------
61
+ class TrexConfigDict(dict):
62
+ """Allow both dict-style and attribute-style access."""
63
+ def __getattr__(self, key):
64
+ try:
65
+ return self[key]
66
+ except KeyError as e:
67
+ raise AttributeError(key) from e
68
+ def __setattr__(self, key, value):
69
+ self[key] = value
70
+
71
+
72
+ # Default base config and jump configs (mirrors trex.ts)
73
+ DEFAULT_TREX_BASE: Dict[str, Any] = {
74
+ "dropVelocity": -5,
75
+ "flashOff": 175,
76
+ "flashOn": 100,
77
+ "height": 47,
78
+ "heightDuck": 25,
79
+ "introDuration": 1500,
80
+ "speedDropCoefficient": 3,
81
+ "spriteWidth": 262,
82
+ "startXPos": 10, # <--- Başlangıç X pozisyonunu tanımla
83
+ "width": 44,
84
+ "widthDuck": 59,
85
+ "invertJump": False,
86
+ # Optional widths used in alt game states
87
+ "widthCrashed": None,
88
+ "widthJump": None,
89
+ }
90
+
91
+ SLOW_JUMP: Dict[str, Any] = {
92
+ "gravity": 0.25,
93
+ "maxJumpHeight": 50,
94
+ "minJumpHeight": 45,
95
+ "initialJumpVelocity": -20,
96
+ }
97
+
98
+ NORMAL_JUMP: Dict[str, Any] = {
99
+ "gravity": 0.6,
100
+ "maxJumpHeight": 30,
101
+ "minJumpHeight": 30,
102
+ "initialJumpVelocity": -10,
103
+ }
104
+
105
+
106
+ # ---------------- Collision boxes ------------------
107
+ collisionBoxes_trex: Dict[str, List[CollisionBox]] = {
108
+ "ducking": [CollisionBox(1, 18, 55, 25)],
109
+ "running": [
110
+ CollisionBox(22, 0, 17, 16),
111
+ CollisionBox(1, 18, 30, 9),
112
+ CollisionBox(10, 35, 14, 8),
113
+ CollisionBox(1, 24, 29, 5),
114
+ CollisionBox(5, 30, 21, 4),
115
+ CollisionBox(9, 34, 15, 4),
116
+ ],
117
+ }
118
+
119
+
120
+ # ---------------- Status enum ------------------
121
+ class Status:
122
+ CRASHED = 0
123
+ DUCKING = 1
124
+ JUMPING = 2
125
+ RUNNING = 3
126
+ WAITING = 4
127
+
128
+
129
+ # Animation frames (pixels from sprite origin)
130
+ BLINK_TIMING = 7000
131
+ # Gözün kapalı kalacağı (ms) — orijinale göre daha kısa
132
+ BLINK_CLOSED_MS = 120
133
+ FrameInfo = Dict[str, Any]
134
+
135
+ # DÜZELTME: Orijinal TS kodundaki gibi, WAITING için Göz Açık (ilk frame 0), Göz Kapalı (ikinci frame 44)
136
+ animFrames: Dict[int, FrameInfo] = {
137
+ Status.WAITING: {"frames": [0, 44], "msPerFrame": 1000 / 3},
138
+ Status.RUNNING: {"frames": [88, 132], "msPerFrame": 1000 / 12},
139
+ Status.CRASHED: {"frames": [220], "msPerFrame": 1000 / 60},
140
+ Status.JUMPING: {"frames": [0], "msPerFrame": 1000 / 60},
141
+ Status.DUCKING: {"frames": [264, 323], "msPerFrame": 1000 / 8},
142
+ }
143
+
144
+
145
+ class Trex:
146
+ # resourceProvider: ConfigProvider & GameStateProvider & ImageSpriteProvider & (opt) GeneratedSoundFxProvider
147
+
148
+ def __init__(self, canvas: pygame.Surface, spritePos: Dict[str, int], resourceProvider: Any):
149
+ self.canvasCtx: pygame.Surface = canvas
150
+ self.spritePos: Dict[str, int] = spritePos
151
+ self.resourceProvider = resourceProvider
152
+
153
+ # Config (dict-like) : default base + normal jump
154
+ self.config = TrexConfigDict(**{**DEFAULT_TREX_BASE, **NORMAL_JUMP})
155
+
156
+ # Position / state
157
+ self.playingIntro: bool = False
158
+ self.xPos: int = 0
159
+ self.yPos: int = 0
160
+ self.xInitialPos: int = self.config.startXPos
161
+ self.groundYPos: int = 0
162
+ self.jumpCount: int = 0
163
+ self.ducking: bool = False
164
+ self.blinkCount: int = 0
165
+ self.jumping: bool = False
166
+ self.speedDrop: bool = False
167
+ self.status: int = Status.WAITING
168
+ self.reachedMinHeight: bool = False
169
+ self.altGameModeEnabled: bool = False
170
+ self.flashing: bool = False
171
+
172
+ # Animation / time
173
+ self.currentFrameIndex: int = 0 # frame index (0 or 1 for WAITING/RUNNING/DUCKING)
174
+ self.currentAnimFrames: List[int] = [] # x coordinates from sprite sheet
175
+ self.blinkDelay: int = 0
176
+ self.animStartTime: int = 0 # Used for blink timing
177
+ self.timer: float = 0.0 # Used for general frame advancement
178
+ self.msPerFrame: float = 1000.0 / FPS
179
+
180
+ # Motion / jump
181
+ self.jumpVelocity: float = 0.0
182
+ self.minJumpHeight: int = 0
183
+
184
+ # Ground position
185
+ runnerDefaultDimensions = DEFAULT_DIMENSIONS
186
+ runnerBottomPadding = self.resourceProvider.getConfig().bottomPad
187
+ assert runnerBottomPadding is not None
188
+ self.groundYPos = runnerDefaultDimensions.height - self.config.height - int(runnerBottomPadding)
189
+ self.yPos = self.groundYPos
190
+ self.minJumpHeight = self.groundYPos - self.config.minJumpHeight
191
+ self.xPos = self.xInitialPos
192
+
193
+ # İlk update WAITING olarak çağrılacak
194
+ self.update(0, Status.WAITING)
195
+
196
+ # ---------------- Speed / config ------------------
197
+ def enableSlowConfig(self):
198
+ jump = SLOW_JUMP if getattr(self.resourceProvider, "hasSlowdown", False) else NORMAL_JUMP
199
+ for k, v in jump.items():
200
+ self.config[k] = v
201
+ self.adjustAltGameConfigForSlowSpeed()
202
+
203
+ def enableAltGameMode(self, spritePos: Dict[str, int]):
204
+ self.altGameModeEnabled = True
205
+ self.spritePos = spritePos
206
+ spriteDefinition = self.resourceProvider.getSpriteDefinition()
207
+ # Adjust bottom placement
208
+ self.groundYPos = DEFAULT_DIMENSIONS.height - self.config.height - spriteDefinition.get("bottomPad", 10)
209
+ self.yPos = self.groundYPos
210
+ self.reset()
211
+
212
+ def adjustAltGameConfigForSlowSpeed(self, gravityValue: Optional[float] = None):
213
+ if getattr(self.resourceProvider, "hasSlowdown", False):
214
+ if gravityValue is not None:
215
+ self.config["gravity"] = gravityValue / 1.5
216
+ self.config["minJumpHeight"] = int(self.config["minJumpHeight"] * 1.5)
217
+ self.config["maxJumpHeight"] = int(self.config["maxJumpHeight"] * 1.5)
218
+ self.config["initialJumpVelocity"] *= 1.5
219
+
220
+ # ---------------- Visual / state ------------------
221
+ def setFlashing(self, status: bool):
222
+ self.flashing = status
223
+
224
+ def setJumpVelocity(self, setting: float):
225
+ self.config["initialJumpVelocity"] = -setting
226
+ self.config["dropVelocity"] = -setting / 2.0
227
+
228
+ # <--- === GÜNCELLENMİŞ UPDATE FONKSİYONU (Orijinal TS Mantığına Daha Yakın) === --->
229
+ def update(self, deltaTime: float, status: Optional[int] = None):
230
+ self.timer += deltaTime # Genel zamanlayıcıyı ilerlet
231
+
232
+ # Update the status if changed
233
+ if status is not None and self.status != status:
234
+ self.status = status
235
+ self.currentFrameIndex = 0
236
+ self.msPerFrame = animFrames[status]["msPerFrame"]
237
+ self.currentAnimFrames = animFrames[status]["frames"]
238
+ self.timer = 0 # Reset general timer only when status changes
239
+ if status == Status.WAITING:
240
+ self.animStartTime = getTimeStamp()
241
+ self._setBlinkDelay()
242
+
243
+ # Game intro animation
244
+ if self.playingIntro and self.xPos < self.config.startXPos:
245
+ self.xPos += round((self.config.startXPos / self.config.introDuration) * deltaTime)
246
+
247
+ # --- Drawing and Animation Frame Logic ---
248
+ if self.status == Status.WAITING:
249
+ self._blink(getTimeStamp()) # Blink handles its own drawing and timing
250
+ else:
251
+ # Draw current frame for non-WAITING states
252
+ sprite_x_to_draw = self.currentAnimFrames[self.currentFrameIndex]
253
+ self.draw(sprite_x_to_draw, 0)
254
+
255
+ # Advance frame based on timer for RUNNING and DUCKING
256
+ # Orijinal TS'deki gibi, yanıp sönme durumunda da kare ilerler
257
+ # Sadece çizim sırasında alpha değişir.
258
+ if self.timer >= self.msPerFrame:
259
+ if self.status in [Status.RUNNING, Status.DUCKING]:
260
+ self.currentFrameIndex = (self.currentFrameIndex + 1) % len(self.currentAnimFrames)
261
+ self.timer = 0 # Reset general timer after frame change
262
+
263
+ # Speed drop becomes duck once on ground
264
+ if self.speedDrop and self.yPos == self.groundYPos:
265
+ self.speedDrop = False
266
+ self.setDuck(True)
267
+ # <--- === UPDATE BİTİŞ === --->
268
+
269
+ def draw(self, x: int, y: int):
270
+ # Force WAITING state to always use the blink frame, regardless of caller.
271
+ if self.status == Status.WAITING:
272
+ wait_frames = animFrames[Status.WAITING]["frames"]
273
+ if wait_frames:
274
+ idx = self.currentFrameIndex % len(wait_frames)
275
+ x = wait_frames[idx]
276
+ runnerImageSprite: pygame.Surface = self.resourceProvider.getRunnerImageSprite()
277
+ assert runnerImageSprite is not None
278
+
279
+ sourceX = x
280
+ sourceY = y
281
+ sourceWidth = self.config.widthDuck if (self.ducking and self.status != Status.CRASHED) else self.config.width
282
+ sourceHeight = self.config.height
283
+
284
+ outputHeight = sourceHeight
285
+ outputWidth = self.config.width if self.status != Status.CRASHED else (self.config.widthCrashed or self.config.width)
286
+
287
+ # Alt-mode width adjustments
288
+ if self.altGameModeEnabled:
289
+ if self.jumping and self.status != Status.CRASHED and self.config.widthJump:
290
+ sourceWidth = self.config.widthJump
291
+ elif self.status == Status.CRASHED and self.config.widthCrashed:
292
+ sourceWidth = self.config.widthCrashed
293
+
294
+ # Pygame'de HIDPI yönetimi farklıdır, scale faktörünü doğrudan kullanmıyoruz
295
+ # Sprite tanımındaki x,y,w,h değerlerini doğrudan kullanacağız
296
+ # IS_HIDPI kontrolü kaldırıldı
297
+ scale = 1 # Varsayılan ölçek
298
+
299
+ sx = int(self.spritePos["x"] + sourceX) # * scale kaldırıldı
300
+ sy = int(self.spritePos["y"] + sourceY) # * scale kaldırıldı
301
+ sw = int(sourceWidth) # * scale kaldırıldı
302
+ sh = int(sourceHeight) # * scale kaldırıldı
303
+
304
+ # Koordinatların geçerli olup olmadığını kontrol et
305
+ img_w, img_h = runnerImageSprite.get_size()
306
+ # spritePos genellikle LDPI içindir, HDPI sprite kullanıyorsak düzeltme gerekebilir
307
+ # Şimdilik LDPI varsayalım
308
+ if sx < 0 or sy < 0 or sx + sw > img_w or sy + sh > img_h:
309
+ # print(f"Uyarı: Geçersiz kaynak koordinatları: Rect({sx}, {sy}, {sw}, {sh}), Img Size: ({img_w}, {img_h})")
310
+ return
311
+
312
+ src_rect = pygame.Rect(sx, sy, sw, sh)
313
+
314
+ # Target size (logical output) - Ölçekleme yoksa kaynakla aynı
315
+ dw = int(sourceWidth) # Output width düzeltmesi
316
+ dh = int(sourceHeight) # Output height düzeltmesi
317
+
318
+ # Hedef boyutları config'den al (ducking durumu için)
319
+ dw_config = self.config.widthDuck if (self.ducking and self.status != Status.CRASHED) else outputWidth
320
+ dh_config = outputHeight
321
+ dw = int(dw_config)
322
+ dh = int(dh_config)
323
+
324
+
325
+ # Alt jump xOffset (optional in sprite definition)
326
+ dx = self.xPos
327
+ if self.altGameModeEnabled and self.jumping and self.status != Status.CRASHED:
328
+ # Alt game mode desteklenmiyor, bu bloğu yoksayabiliriz şimdilik
329
+ pass
330
+ # spriteDefinition = self.resourceProvider.getSpriteDefinition()
331
+ # tRexDef = spriteDefinition.get("tRex")
332
+ # if tRexDef and isinstance(tRexDef, dict):
333
+ # xo = tRexDef.get("jumping", {}).get("xOffset", 0)
334
+ # dx = self.xPos - xo # scale kaldırıldı
335
+
336
+ # Flashing alpha kontrolü
337
+ overall_alpha = None
338
+ if self.flashing:
339
+ # Orijinal TS mantığına daha yakın: Genel zamanlayıcıyı kullan
340
+ flash_timer_check = self.timer % (self.config.flashOn + self.config.flashOff)
341
+ if flash_timer_check < self.config.flashOn:
342
+ overall_alpha = 128 # Yarı saydam
343
+ # else: Tam opak (alpha=None) - _blit_region_alpha bunu halleder
344
+
345
+ # Per-pixel alpha safe blit (prevents black boxes)
346
+ _blit_region_alpha(
347
+ dst=self.canvasCtx,
348
+ src=runnerImageSprite,
349
+ src_rect=src_rect,
350
+ dest_xy=(int(dx), int(self.yPos)),
351
+ dest_wh=(dw, dh), # Hedef boyutları kullan
352
+ overall_alpha=overall_alpha,
353
+ )
354
+
355
+ def _setBlinkDelay(self):
356
+ # Orijinal TS Math.ceil(Math.random() * BLINK_TIMING) -> 1 ile BLINK_TIMING arası
357
+ self.blinkDelay = random.randint(1, BLINK_TIMING)
358
+
359
+ # <--- === GÜNCELLENMİŞ BLINK FONKSİYONU (Orijinal TS Mantığına Daha Yakın) === --->
360
+ def _blink(self, time_ms: int):
361
+ """Make t-rex blink at random intervals."""
362
+ deltaTime = time_ms - self.animStartTime
363
+
364
+ # WAITING animasyon bilgilerini al
365
+ wait_anim = animFrames[Status.WAITING]
366
+ wait_frames = wait_anim["frames"] # [Göz Açık X, Göz Kapalı X]
367
+ closed_eye_duration = BLINK_CLOSED_MS # Gözün kapalı kalma süresi (ms)
368
+
369
+ # Geçerli sprite X koordinatını belirle
370
+ sprite_x_to_draw = wait_frames[self.currentFrameIndex]
371
+
372
+ # Dinozoru çiz
373
+ self.draw(sprite_x_to_draw, 0)
374
+
375
+ # Zamanlamayı kontrol et ve kareyi değiştir (Orijinal TS Mantığı)
376
+ # Eğer göz kırpma zamanı geldiyse (ve şu an göz açıksa - index 0)
377
+ if self.currentFrameIndex == 0 and deltaTime >= self.blinkDelay:
378
+ self.currentFrameIndex = 1 # Göz kapalı kareye geç (index 1)
379
+ self.animStartTime = time_ms # Gözün kapalı kalma süresini başlat
380
+
381
+ # Eğer göz kapalıysa (index 1) ve kapalı kalma süresi dolduysa
382
+ elif self.currentFrameIndex == 1 and deltaTime >= closed_eye_duration:
383
+ self.currentFrameIndex = 0 # Göz açık kareye geç (index 0)
384
+ self._setBlinkDelay() # Yeni bir blink zamanı ayarla
385
+ self.animStartTime = time_ms # Gözün açık kalma süresini başlat
386
+ self.blinkCount += 1
387
+ # <--- === BLINK BİTİŞ === --->
388
+
389
+ # ---------------- Jump / motion (DEĞİŞİKLİK YOK) ------------------
390
+ def startJump(self, speed: float):
391
+ if not self.jumping:
392
+ # self.update(0, Status.JUMPING) # update'i direkt çağırma, state'i ayarla
393
+ self.status = Status.JUMPING
394
+ self.currentFrameIndex = 0
395
+ self.timer = 0
396
+ self.msPerFrame = animFrames[Status.JUMPING]["msPerFrame"]
397
+ self.currentAnimFrames = animFrames[Status.JUMPING]["frames"]
398
+
399
+ self.jumpVelocity = self.config.initialJumpVelocity - (speed / 10.0)
400
+ self.jumping = True
401
+ self.reachedMinHeight = False
402
+ self.speedDrop = False
403
+ if self.config.invertJump:
404
+ self.minJumpHeight = self.groundYPos + self.config.minJumpHeight
405
+
406
+ def endJump(self):
407
+ if self.reachedMinHeight and self.jumpVelocity < self.config.dropVelocity:
408
+ self.jumpVelocity = self.config.dropVelocity
409
+
410
+ def updateJump(self, deltaTime: float):
411
+ # msPerFrame JUMPING durumundan alınmalı
412
+ msPerFrame = animFrames[Status.JUMPING]["msPerFrame"]
413
+ framesElapsed = (deltaTime / msPerFrame) if msPerFrame > 0 else 1.0
414
+
415
+ if self.speedDrop:
416
+ self.yPos += round(self.jumpVelocity * self.config.speedDropCoefficient * framesElapsed)
417
+ elif self.config.invertJump:
418
+ self.yPos -= round(self.jumpVelocity * framesElapsed)
419
+ else:
420
+ self.yPos += round(self.jumpVelocity * framesElapsed)
421
+
422
+ self.jumpVelocity += self.config.gravity * framesElapsed
423
+
424
+ # Min height reached?
425
+ if (self.config.invertJump and (self.yPos > self.minJumpHeight)) or \
426
+ ((not self.config.invertJump) and (self.yPos < self.minJumpHeight)) or \
427
+ self.speedDrop:
428
+ self.reachedMinHeight = True
429
+
430
+ # Max height logic
431
+ if (self.config.invertJump and (self.yPos > -self.config.maxJumpHeight)) or \
432
+ ((not self.config.invertJump) and (self.yPos < self.config.maxJumpHeight)) or \
433
+ self.speedDrop:
434
+ self.endJump()
435
+
436
+ # Back down at ground level. Jump completed.
437
+ if (self.config.invertJump and (self.yPos < self.groundYPos)) or \
438
+ ((not self.config.invertJump) and (self.yPos > self.groundYPos)):
439
+ self.reset()
440
+ self.jumpCount += 1
441
+ if getattr(self.resourceProvider, "hasAudioCues", False):
442
+ if hasattr(self.resourceProvider, "getGeneratedSoundFx"):
443
+ try:
444
+ self.resourceProvider.getGeneratedSoundFx().loopFootSteps()
445
+ except Exception:
446
+ pass
447
+
448
+ def setSpeedDrop(self):
449
+ self.speedDrop = True
450
+ self.jumpVelocity = 1
451
+
452
+ def setDuck(self, isDucking: bool):
453
+ if isDucking and self.status != Status.DUCKING:
454
+ # self.update(0, Status.DUCKING) # update'i çağırma
455
+ self.status = Status.DUCKING
456
+ self.currentFrameIndex = 0
457
+ self.timer = 0
458
+ self.msPerFrame = animFrames[Status.DUCKING]["msPerFrame"]
459
+ self.currentAnimFrames = animFrames[Status.DUCKING]["frames"]
460
+ self.ducking = True
461
+ elif self.status == Status.DUCKING and not isDucking:
462
+ # self.update(0, Status.RUNNING) # update'i çağırma
463
+ self.status = Status.RUNNING
464
+ self.currentFrameIndex = 0
465
+ self.timer = 0
466
+ self.msPerFrame = animFrames[Status.RUNNING]["msPerFrame"]
467
+ self.currentAnimFrames = animFrames[Status.RUNNING]["frames"]
468
+ self.ducking = False
469
+
470
+ def reset(self):
471
+ self.xPos = self.xInitialPos # reset'te xInitialPos'a dönmeli
472
+ self.yPos = self.groundYPos
473
+ self.jumpVelocity = 0
474
+ self.jumping = False
475
+ self.ducking = False
476
+
477
+ self.status = Status.RUNNING # Sadece durumu ayarla
478
+ self.currentFrameIndex = 0 # Animasyonu sıfırla
479
+ self.timer = 0 # Zamanlayıcıyı sıfırla
480
+ self.currentAnimFrames = animFrames[Status.RUNNING]["frames"] # Kareleri ayarla
481
+ self.msPerFrame = animFrames[Status.RUNNING]["msPerFrame"] # Hızı ayarla
482
+
483
+ self.speedDrop = False
484
+ self.jumpCount = 0
485
+
486
+ def getCollisionBoxes(self) -> List[CollisionBox]:
487
+ return collisionBoxes_trex["ducking"] if self.ducking else collisionBoxes_trex["running"]
488
+
pydino/utils.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils.py
2
+ # 1:1 port of utils.ts for a pygame-based Chrome Dino port.
3
+
4
+ from __future__ import annotations
5
+ import random
6
+ import time
7
+ try:
8
+ from constants import IS_IOS
9
+ except ImportError:
10
+ IS_IOS = False
11
+
12
+
13
+ def get_random_num(min_val: int, max_val: int) -> int:
14
+ """Inclusive integer random: [min_val, max_val]."""
15
+ # TS: Math.floor(Math.random() * (max - min + 1)) + min
16
+ return random.randint(min_val, max_val)
17
+
18
+
19
+ def get_time_stamp() -> int:
20
+ """Current timestamp in milliseconds.
21
+ TS: IS_IOS ? Date().getTime() : performance.now()
22
+ """
23
+ if IS_IOS:
24
+ # Wall-clock ms
25
+ return int(time.time() * 1000)
26
+ # High-resolution monotonic ms
27
+ return int(time.perf_counter() * 1000)
28
+
29
+
30
+ # --- Aliases for TS-style imports (camelCase) ---
31
+ getRandomNum = get_random_num
32
+ getTimeStamp = get_time_stamp
watch_model.py ADDED
@@ -0,0 +1,650 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ NeuroDino - Model Watcher (Inference Mode)
4
+
5
+ Watch your trained brain play the game without training.
6
+ Press 'S' to toggle between 60 FPS and Unlimited FPS.
7
+ Press 'R' to restart the game.
8
+ Press 'Q' or ESC to quit.
9
+
10
+ Usage:
11
+ python watch_model.py # Load best_brain.pkl
12
+ python watch_model.py --brain path.pkl # Load specific brain file
13
+ python watch_model.py --fast # Start in unlimited FPS mode
14
+ python watch_model.py --silent # No display, just run and print scores
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import argparse
20
+ import pickle
21
+ import pygame
22
+
23
+ # Add pydino directory to sys.path
24
+ pydino_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pydino")
25
+ sys.path.append(pydino_path)
26
+
27
+ from dimensions import Dimensions
28
+ from pydino.runner import Runner, Config
29
+ from pydino.trex import Trex, Status as TrexStatus
30
+ from neurodino.neuro_trex import NeuroTrex
31
+ from neurodino.brain import Brain
32
+ import numpy as np
33
+ import math
34
+
35
+ # Constants (must match training)
36
+ GAME_HEIGHT = 150
37
+ MAX_OBSTACLE_WIDTH = 75
38
+ MAX_TTI_FRAMES = 50.0
39
+ DUCK_THRESHOLD_Y = 75
40
+
41
+
42
+ class ModelWatcher(Runner):
43
+ """
44
+ Simplified runner for watching a single trained brain play.
45
+ No training, no population - just inference.
46
+ """
47
+
48
+ def __init__(self, screen, dimensions, brain: Brain, target_fps=60, silent=False):
49
+ super().__init__(screen, dimensions, use_audio=not silent)
50
+
51
+ self.brain = brain
52
+ self.target_fps = target_fps
53
+ self.score = 0
54
+ self.high_score = 0
55
+ self.games_played = 0
56
+ self.silent = silent
57
+ self.last_10k = 0 # Track last 10K milestone for silent mode
58
+
59
+ # Night mode variables (matching original Runner)
60
+ self.inverted = False
61
+ self.invert_timer = 0
62
+ self.invert_trigger = False
63
+ self.NIGHT_ALPHA_MAX = 180
64
+ self.VISUAL_INVERT_MS = 1000
65
+ self._night_overlay = pygame.Surface((dimensions.width, dimensions.height), pygame.SRCALPHA)
66
+
67
+ # Visualization toggles
68
+ self.viz_bezier = True # B - Use Bezier curves
69
+ self.viz_width = True # W - Edge width proportional to weights
70
+ self.viz_opacity = True # O - Edge opacity proportional to weights
71
+ self.viz_color = True # C - Edge color by weight sign (blue/red vs gray)
72
+
73
+ # Replace default trex with our NeuroTrex
74
+ self._setup_neuro_trex()
75
+
76
+ def _setup_neuro_trex(self):
77
+ """Setup a single NeuroTrex with the loaded brain."""
78
+ self.dino = NeuroTrex(self.screen, self.sprite_def["tRex"], self)
79
+ self.dino.brain = self.brain
80
+ self.dino.visible = True
81
+ self.trex = self.dino # For compatibility with base Runner
82
+
83
+ def _get_inputs(self):
84
+ """Get inputs for the brain (same as training)."""
85
+ dino = self.dino
86
+ speed = self.current_speed / self.config.maxSpeed
87
+
88
+ # Dino state
89
+ ground_y = dino.groundYPos
90
+ max_jump = dino.config.maxJumpHeight
91
+
92
+ dino_y_normalized = 0.0
93
+ if dino.jumping:
94
+ height_above_ground = ground_y - dino.yPos
95
+ dino_y_normalized = min(1.0, max(0.0, height_above_ground / max_jump))
96
+
97
+ dino_velocity = 0.0
98
+ if dino.jumping:
99
+ dino_velocity = max(-1.0, min(1.0, dino.jumpVelocity / 10.0))
100
+
101
+ is_airborne = 1.0 if dino.jumping else 0.0
102
+ is_ducking = 1.0 if dino.ducking else 0.0
103
+
104
+ # Obstacles
105
+ obs1_dist = 0.0
106
+ obs1_action = 0.0
107
+ obs1_w = 0.0
108
+ obs2_dist = 0.0
109
+ obs2_action = 0.0
110
+ obs2_w = 0.0
111
+ gap = 0.0
112
+
113
+ if self.horizon and self.horizon.obstacles:
114
+ dino_front = dino.xPos
115
+ future_obstacles = [o for o in self.horizon.obstacles if o.xPos > dino_front]
116
+ future_obstacles.sort(key=lambda o: o.xPos)
117
+
118
+ if len(future_obstacles) > 0:
119
+ o1 = future_obstacles[0]
120
+ dist1 = o1.xPos - dino.xPos
121
+ tti1 = dist1 / max(1.0, self.current_speed)
122
+ obs1_dist = 1.0 - min(1.0, tti1 / MAX_TTI_FRAMES)
123
+ obs1_action = 1.0 if o1.yPos < DUCK_THRESHOLD_Y else 0.0
124
+ obs1_w = min(1.0, o1.width / MAX_OBSTACLE_WIDTH)
125
+
126
+ if len(future_obstacles) > 1:
127
+ o2 = future_obstacles[1]
128
+ dist2 = o2.xPos - dino.xPos
129
+ tti2 = dist2 / max(1.0, self.current_speed)
130
+ obs2_dist = 1.0 - min(1.0, tti2 / MAX_TTI_FRAMES)
131
+ obs2_action = 1.0 if o2.yPos < DUCK_THRESHOLD_Y else 0.0
132
+ obs2_w = min(1.0, o2.width / MAX_OBSTACLE_WIDTH)
133
+
134
+ raw_gap = o2.xPos - (o1.xPos + o1.width)
135
+ time_gap = raw_gap / max(1.0, self.current_speed)
136
+ gap = 1.0 - min(1.0, time_gap / 15.0)
137
+
138
+ return np.array([
139
+ obs1_dist, obs1_action, obs1_w,
140
+ obs2_dist, obs2_action, obs2_w,
141
+ speed, gap,
142
+ dino_y_normalized, dino_velocity,
143
+ is_airborne, is_ducking
144
+ ])
145
+
146
+ def update(self):
147
+ """Game loop with AI control."""
148
+ now = pygame.time.get_ticks()
149
+ delta = 1000.0 / 60 # Fixed 60 FPS physics
150
+ self.time_ms = now
151
+
152
+ # Clear screen (skip in silent mode)
153
+ if not self.silent:
154
+ self.screen.fill((247, 247, 247))
155
+
156
+ # Update game state
157
+ if self.playing:
158
+ self.running_time += delta
159
+
160
+ has_obstacles = self.running_time > self.config.clearTime
161
+
162
+ # Night mode logic (from original Runner) - always update timer, even in turbo
163
+ show_night_mode = self.inverted
164
+ if self.playing:
165
+ actual_score = int(self.distance_ran * 0.025)
166
+
167
+ # Timer-based fade cycle
168
+ if self.invert_timer > self.config.invertFadeDuration:
169
+ self.invert_timer = 0
170
+ self.invert_trigger = False
171
+ self.inverted = not self.inverted
172
+ elif self.invert_timer > 0:
173
+ self.invert_timer += delta
174
+ else:
175
+ # Trigger at each invertDistance milestone
176
+ if actual_score > 0 and (actual_score % self.config.invertDistance == 0) and not self.invert_trigger:
177
+ self.invert_timer += delta
178
+ self.inverted = not self.inverted
179
+ self.invert_trigger = True
180
+ elif (actual_score % self.config.invertDistance) != 0:
181
+ self.invert_trigger = False
182
+
183
+ # Draw horizon FIRST (includes moon, stars, clouds, ground)
184
+ self.horizon.update(delta, self.current_speed, has_obstacles, show_night_mode)
185
+
186
+ # AI Decision and Dino update (draws dino ON TOP of horizon)
187
+ if self.playing and not self.crashed and self.dino.status != TrexStatus.CRASHED:
188
+ inputs = self._get_inputs()
189
+ outputs = self.dino.brain.predict(inputs)
190
+ action = np.argmax(outputs)
191
+ self.dino.act(action)
192
+ self.dino.update(delta) # This draws dino
193
+
194
+ if self.dino.jumping:
195
+ self.dino.updateJump(delta)
196
+
197
+ if self.playing and not self.silent:
198
+ self.distance_meter.update(delta, math.ceil(self.distance_ran))
199
+
200
+ # Collision detection
201
+ if self.playing and not self.crashed:
202
+ if has_obstacles and self.horizon.obstacles:
203
+ for obstacle in self.horizon.obstacles:
204
+ if self._check_for_collision(obstacle, self.dino):
205
+ self.dino.update(100, TrexStatus.CRASHED)
206
+ self.crashed = True
207
+ self._on_game_over()
208
+ break
209
+
210
+ if not self.crashed:
211
+ self.distance_ran += self.current_speed * (delta / self.ms_per_frame)
212
+ self.score = int(self.distance_ran * 0.025)
213
+
214
+ # Silent mode: Print every 10K
215
+ if self.silent:
216
+ current_10k = self.score // 10000
217
+ if current_10k > self.last_10k:
218
+ self.last_10k = current_10k
219
+ print(f"📈 Score: {self.score:,}")
220
+
221
+ if self.current_speed < self.config.maxSpeed:
222
+ self.current_speed += self.config.acceleration
223
+
224
+ # Draw game over panel if crashed (skip in silent mode)
225
+ if not self.silent and self.crashed and self.game_over_panel:
226
+ self.game_over_panel.draw(False, self.dino)
227
+
228
+ # Apply night overlay (skip in silent mode)
229
+ if not self.silent:
230
+ fade_factor = self._get_invert_fade_factor()
231
+ if fade_factor > 0:
232
+ self._apply_night_overlay(fade_factor)
233
+
234
+ # Draw stats overlay (skip in silent mode)
235
+ # Blit game surface to main screen (centered)
236
+ if not self.silent and hasattr(self, 'main_screen'):
237
+ # Clear main screen
238
+ self.main_screen.fill((247, 247, 247))
239
+ # Blit game surface centered
240
+ offset = getattr(self, 'game_offset', 0)
241
+ self.main_screen.blit(self.screen, (offset, 0))
242
+ # Draw stats and brain on main screen
243
+ self._draw_stats(self.main_screen, offset)
244
+ self._draw_brain(self.main_screen)
245
+
246
+ def _get_invert_fade_factor(self):
247
+ """Calculate fade factor for night mode transition (from original Runner)."""
248
+ T = self.config.invertFadeDuration
249
+ t = self.invert_timer
250
+ vis = self.VISUAL_INVERT_MS
251
+
252
+ if self.inverted:
253
+ if t == 0:
254
+ return 1.0
255
+ elif t < vis:
256
+ return self._ease_in_out_cubic(t / vis)
257
+ elif t <= (T - vis):
258
+ return 1.0
259
+ else:
260
+ return self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis))
261
+ else:
262
+ if t > 0 and t > (T - vis):
263
+ return self._ease_in_out_cubic(1.0 - ((t - (T - vis)) / vis))
264
+ return 0.0
265
+
266
+ def _ease_in_out_cubic(self, t):
267
+ """CSS-like ease-in-out for smooth transitions."""
268
+ if t <= 0.0:
269
+ return 0.0
270
+ if t >= 1.0:
271
+ return 1.0
272
+ if t < 0.5:
273
+ return 4.0 * t * t * t
274
+ return 1.0 - ((-2.0 * t + 2.0) ** 3) / 2.0
275
+
276
+ def _apply_night_overlay(self, fade_factor):
277
+ if fade_factor <= 0.0:
278
+ return
279
+
280
+ try:
281
+ # Get pixels as array (only for game area, not brain viz)
282
+ game_rect = pygame.Rect(0, 0, self.screen.get_width(), 150)
283
+ game_surface = self.screen.subsurface(game_rect)
284
+
285
+ # Convert to array, invert, then blend
286
+ pixels = pygame.surfarray.pixels3d(game_surface)
287
+ inverted = 255 - pixels
288
+
289
+ # Blend between original and inverted based on fade_factor
290
+ blended = pixels * (1.0 - fade_factor) + inverted * fade_factor
291
+ np.copyto(pixels, blended.astype(np.uint8))
292
+ del pixels # Release surface lock
293
+ except Exception:
294
+ # Fallback to dark overlay if pixel manipulation fails
295
+ alpha = int(self.NIGHT_ALPHA_MAX * fade_factor)
296
+ if alpha > 0:
297
+ self._night_overlay.fill((0, 0, 0, alpha))
298
+ self.screen.blit(self._night_overlay, (0, 0))
299
+
300
+ def _on_game_over(self):
301
+ """Handle game over."""
302
+ self.games_played += 1
303
+ if self.score > self.high_score:
304
+ self.high_score = self.score
305
+
306
+ if self.silent:
307
+ print(f"💀 ÖLDÜ! Score: {self.score:,} | High: {self.high_score:,} | Game #{self.games_played}")
308
+ else:
309
+ print(f"Game {self.games_played}: Score {self.score} | High Score: {self.high_score}")
310
+
311
+ def restart_game(self):
312
+ """Restart the game."""
313
+ self.crashed = False
314
+ self.playing = True
315
+ self.distance_ran = 0
316
+ self.score = 0
317
+ self.last_10k = 0 # Reset 10K tracker
318
+ self.current_speed = self.config.speed
319
+ self.running_time = 0
320
+
321
+ # Reset night mode
322
+ self.inverted = False
323
+ self.invert_timer = 0
324
+ self.invert_trigger = False
325
+
326
+ self.horizon.reset()
327
+ if not self.silent:
328
+ self.distance_meter.reset()
329
+
330
+ # Reset dino
331
+ self._setup_neuro_trex()
332
+ self.dino.update(0, TrexStatus.RUNNING)
333
+
334
+ def _draw_stats(self, target_screen, offset=0):
335
+ """Draw stats overlay - only speed."""
336
+ font = pygame.font.Font(None, 28)
337
+ txt = font.render(f"Speed: {self.current_speed:.1f}", True, (80, 80, 80))
338
+ target_screen.blit(txt, (offset + 10, 10))
339
+
340
+ def _draw_brain(self, target_screen):
341
+ """Draw brain visualization."""
342
+ brain = self.brain
343
+ if not hasattr(brain, "last_inputs"):
344
+ return
345
+
346
+ start_y = 150
347
+ w = target_screen.get_width()
348
+ h = target_screen.get_height() - start_y
349
+
350
+ # Background - white
351
+ surf = pygame.Surface((w, h))
352
+ surf.fill((255, 255, 255))
353
+ target_screen.blit(surf, (0, start_y))
354
+
355
+ # Layout - use screen width for positioning
356
+ layer_x = [80, w // 2, w - 80]
357
+ input_y = np.linspace(start_y + 30, start_y + h - 30, brain.input_nodes)
358
+ hidden_y = np.linspace(start_y + 20, start_y + h - 20, brain.hidden_nodes)
359
+
360
+ # Center output nodes vertically (3 nodes with 60px spacing)
361
+ center_y = start_y + h // 2
362
+ output_spacing = 80
363
+ output_y = [center_y - output_spacing, center_y, center_y + output_spacing]
364
+
365
+ input_labels = ["O1 TTI", "O1 Act", "O1 W", "O2 TTI", "O2 Act", "O2 W",
366
+ "Speed", "Gap", "DinoY", "DinoVel", "Air", "Duck"]
367
+ output_labels = ["Jump", "Duck", "Run"]
368
+
369
+ font = pygame.font.Font(None, 18)
370
+
371
+ def get_color(val):
372
+ v = max(0, min(1, abs(val)))
373
+ return (int(v*255), int(v*255), int(v*255))
374
+
375
+ def draw_bezier(start, end, color, width=1):
376
+ """Draw a Bezier curve between two points."""
377
+ x1, y1 = start
378
+ x2, y2 = end
379
+ # Control points for smooth curve
380
+ mid_x = (x1 + x2) // 2
381
+ ctrl1 = (mid_x, y1)
382
+ ctrl2 = (mid_x, y2)
383
+
384
+ # Generate curve points
385
+ points = []
386
+ for t in range(0, 21):
387
+ t = t / 20.0
388
+ # Cubic Bezier formula
389
+ x = int((1-t)**3 * x1 + 3*(1-t)**2*t * ctrl1[0] + 3*(1-t)*t**2 * ctrl2[0] + t**3 * x2)
390
+ y = int((1-t)**3 * y1 + 3*(1-t)**2*t * ctrl1[1] + 3*(1-t)*t**2 * ctrl2[1] + t**3 * y2)
391
+ points.append((x, y))
392
+
393
+ if len(points) > 1:
394
+ if width > 1:
395
+ pygame.draw.lines(target_screen, color, False, points, width)
396
+ else:
397
+ pygame.draw.aalines(target_screen, color, False, points)
398
+
399
+ # Draw weights with curves or lines (only strong connections)
400
+ def draw_edge(start, end, weight):
401
+ # Base color based on viz_color toggle
402
+ if self.viz_color:
403
+ # Colored mode: blue = positive, red = negative
404
+ if weight < 0:
405
+ base_color = (255, 0, 0) # Red for negative
406
+ else:
407
+ base_color = (0, 0, 255) # Blue for positive
408
+ else:
409
+ # Gray mode
410
+ base_color = (80, 80, 80)
411
+
412
+ # Apply opacity if enabled - NN-SVG style (linear 0-1, weak weights invisible)
413
+ if self.viz_opacity:
414
+ # Linear scale like NN-SVG: domain([0, 1]).range([0, 1])
415
+ w_norm = min(1.0, abs(weight))
416
+ if w_norm < 0.05:
417
+ return # Skip drawing very weak connections
418
+ # Blend from white background (255,255,255) to base_color based on weight
419
+ # weak = white (invisible on white bg), strong = base_color
420
+ color = (int(255 - (255 - base_color[0]) * w_norm),
421
+ int(255 - (255 - base_color[1]) * w_norm),
422
+ int(255 - (255 - base_color[2]) * w_norm))
423
+ else:
424
+ color = base_color
425
+
426
+ # Apply width if enabled - FCNN style (weak = thin/invisible)
427
+ if self.viz_width:
428
+ # Linear scale: weight 0 -> width 0, weight 1 -> width 3
429
+ width = int(abs(weight) * 3)
430
+ if width < 1:
431
+ return # Skip drawing very thin connections
432
+ else:
433
+ width = 1
434
+
435
+ # Draw Bezier or straight line
436
+ if self.viz_bezier:
437
+ draw_bezier(start, end, color, width)
438
+ else:
439
+ pygame.draw.line(target_screen, color, start, end, width)
440
+
441
+ # Threshold: show all when opacity OFF, filter weak when ON
442
+ threshold = 0.05 if self.viz_opacity else 0.001
443
+
444
+ for i in range(brain.input_nodes):
445
+ for j in range(brain.hidden_nodes):
446
+ weight = brain.weights_ih[j][i]
447
+ if abs(weight) > threshold:
448
+ draw_edge((layer_x[0], int(input_y[i])),
449
+ (layer_x[1], int(hidden_y[j])), weight)
450
+
451
+ for j in range(brain.hidden_nodes):
452
+ for k in range(brain.output_nodes):
453
+ weight = brain.weights_ho[k][j]
454
+ if abs(weight) > threshold:
455
+ draw_edge((layer_x[1], int(hidden_y[j])),
456
+ (layer_x[2], int(output_y[k])), weight)
457
+
458
+ # Draw input nodes
459
+ for i, val in enumerate(brain.last_inputs):
460
+ pos = (layer_x[0], int(input_y[i]))
461
+ pygame.draw.circle(target_screen, (255, 255, 255), pos, 8)
462
+ pygame.draw.circle(target_screen, (51, 51, 51), pos, 8, 1)
463
+ lbl = font.render(f"{input_labels[i]}:{val:.2f}", True, (0, 0, 0))
464
+ target_screen.blit(lbl, (pos[0]-40, pos[1]-12))
465
+
466
+ # Draw hidden nodes
467
+ for i, val in enumerate(brain.last_hidden):
468
+ pos = (layer_x[1], int(hidden_y[i]))
469
+ pygame.draw.circle(target_screen, (255, 255, 255), pos, 6)
470
+ pygame.draw.circle(target_screen, (51, 51, 51), pos, 6, 1)
471
+
472
+ # Draw output nodes
473
+ max_idx = np.argmax(brain.last_outputs)
474
+ for i, val in enumerate(brain.last_outputs):
475
+ color = (0, 255, 0) if i == max_idx else (255, 255, 255)
476
+ pos = (layer_x[2], int(output_y[i]))
477
+ radius = 10 + int(val * 10)
478
+ pygame.draw.circle(target_screen, color, pos, radius)
479
+ pygame.draw.circle(target_screen, (51, 51, 51), pos, radius, 2)
480
+ lbl = font.render(f"{output_labels[i]} ({val:.0%})", True, (0, 0, 0))
481
+ target_screen.blit(lbl, (pos[0]+20, pos[1]-8))
482
+
483
+
484
+ def main():
485
+ parser = argparse.ArgumentParser(description='Watch trained NeuroDino model')
486
+ parser.add_argument('--brain', type=str, default='best_brain.pkl',
487
+ help='Path to brain file (default: best_brain.pkl)')
488
+ parser.add_argument('--fast', action='store_true',
489
+ help='Start in unlimited FPS mode')
490
+ parser.add_argument('--silent', action='store_true',
491
+ help='No display, just run simulation and print scores')
492
+ args = parser.parse_args()
493
+
494
+ # Load brain
495
+ if not os.path.exists(args.brain):
496
+ print(f"Error: Brain file not found: {args.brain}")
497
+ print("Train a model first with: python main_train.py")
498
+ sys.exit(1)
499
+
500
+ try:
501
+ with open(args.brain, "rb") as f:
502
+ data = pickle.load(f)
503
+ if isinstance(data, tuple):
504
+ brain, score = data
505
+ print(f"✅ Loaded brain from {args.brain}")
506
+ print(f" Training score: {score:,}")
507
+ else:
508
+ brain = data
509
+ print(f"✅ Loaded brain from {args.brain} (legacy format)")
510
+ except Exception as e:
511
+ print(f"Error loading brain: {e}")
512
+ sys.exit(1)
513
+
514
+ # Silent mode setup
515
+ if args.silent:
516
+ os.environ['SDL_VIDEODRIVER'] = 'dummy'
517
+ os.environ['SDL_AUDIODRIVER'] = 'dummy'
518
+ print("🔇 SILENT MODE - No display, maximum speed")
519
+ print(" Press Ctrl+C to stop\n")
520
+
521
+ # Initialize pygame
522
+ pygame.init()
523
+
524
+ if not args.silent:
525
+ pygame.display.set_caption("NeuroDino - Model Watcher")
526
+
527
+ # Game area stays fixed at 600x150, neural network area expanded
528
+ dims = Dimensions(width=600, height=150)
529
+ screen = pygame.display.set_mode((900, 850)) # Wider and taller for NN
530
+
531
+ # Create separate surface for game at original size
532
+ game_surface = pygame.Surface((dims.width, dims.height))
533
+ game_offset = (900 - 600) // 2 # Center horizontally
534
+
535
+ clock = pygame.time.Clock()
536
+
537
+ # Create watcher - pass game_surface as the drawing target for the game
538
+ watcher = ModelWatcher(game_surface, dims, brain, silent=args.silent)
539
+ watcher.main_screen = screen # Store main screen for NN drawing
540
+ watcher.game_offset = game_offset # Store offset for centering
541
+ watcher.start()
542
+
543
+ # FPS modes: 0=60fps, 1=unlimited, 2=turbo
544
+ speed_mode = 0
545
+ if args.fast:
546
+ speed_mode = 1
547
+ if args.silent:
548
+ speed_mode = 2
549
+
550
+ turbo_mode = False # Runtime turbo toggle (no drawing)
551
+
552
+ if not args.silent:
553
+ print("\n🎮 Controls:")
554
+ print(" S - Toggle speed (60 FPS → Unlimited → Turbo)")
555
+ print(" B - Toggle Bezier curves")
556
+ print(" W - Toggle edge width proportional to weights")
557
+ print(" O - Toggle edge opacity proportional to weights")
558
+ print(" C - Toggle edge color (Blue/Red vs Gray)")
559
+ print(" R - Restart game")
560
+ print(" Q/ESC - Quit\n")
561
+
562
+
563
+ running = True
564
+ last_log_time = pygame.time.get_ticks()
565
+ frame_count = 0
566
+
567
+ try:
568
+ while running:
569
+ # FPS control based on mode
570
+ if speed_mode == 0:
571
+ clock.tick(60)
572
+ else:
573
+ clock.tick() # Unlimited
574
+
575
+ frame_count += 1
576
+
577
+ for event in pygame.event.get():
578
+ if event.type == pygame.QUIT:
579
+ running = False
580
+ elif event.type == pygame.KEYDOWN and not args.silent:
581
+ if event.key == pygame.K_q or event.key == pygame.K_ESCAPE:
582
+ running = False
583
+ elif event.key == pygame.K_s:
584
+ # Cycle through 3 modes
585
+ speed_mode = (speed_mode + 1) % 3
586
+ turbo_mode = (speed_mode == 2)
587
+ watcher.silent = turbo_mode
588
+
589
+ mode_names = ["60 FPS", "UNLIMITED", "🚀 TURBO (no draw)"]
590
+ print(f"Speed: {mode_names[speed_mode]}")
591
+ elif event.key == pygame.K_r:
592
+ watcher.restart_game()
593
+ print("Game restarted!")
594
+ elif event.key == pygame.K_b:
595
+ watcher.viz_bezier = not watcher.viz_bezier
596
+ print(f"Bezier curves: {'ON' if watcher.viz_bezier else 'OFF'}")
597
+ elif event.key == pygame.K_w:
598
+ watcher.viz_width = not watcher.viz_width
599
+ print(f"Edge width: {'ON' if watcher.viz_width else 'OFF'}")
600
+ elif event.key == pygame.K_o:
601
+ watcher.viz_opacity = not watcher.viz_opacity
602
+ print(f"Edge opacity: {'ON' if watcher.viz_opacity else 'OFF'}")
603
+ elif event.key == pygame.K_c:
604
+ watcher.viz_color = not watcher.viz_color
605
+ print(f"Edge color: {'Blue/Red' if watcher.viz_color else 'Gray'}")
606
+
607
+ watcher.update()
608
+
609
+ if not args.silent and not turbo_mode:
610
+ pygame.display.flip()
611
+
612
+ # Auto-restart on crash
613
+ if watcher.crashed:
614
+ if not args.silent and not turbo_mode:
615
+ pygame.time.wait(500)
616
+ watcher.restart_game()
617
+
618
+ # Logging for Turbo/Silent/Headless Modes
619
+ if args.silent or turbo_mode:
620
+ current_time = pygame.time.get_ticks()
621
+ if current_time - last_log_time > 1000: # 10 seconds
622
+ elapsed_seconds = (current_time - last_log_time) / 1000.0
623
+ real_sps = int(frame_count / elapsed_seconds)
624
+ print(f" [Watch] Score: {watcher.score:,} | High: {watcher.high_score:,} | SPS: {real_sps} (Sim/Sec)")
625
+
626
+ last_log_time = current_time
627
+ frame_count = 0
628
+
629
+ # Update title (only in display mode)
630
+ if not args.silent and not turbo_mode:
631
+ mode_names = ["60", "MAX", "TURBO"]
632
+ fps_val = clock.get_fps()
633
+ if fps_val == float('inf') or fps_val > 99999:
634
+ fps_text = f"MAX ({mode_names[speed_mode]})"
635
+ else:
636
+ fps_text = f"{int(fps_val)} ({mode_names[speed_mode]})"
637
+ pygame.display.set_caption(
638
+ f"NeuroDino | Score: {watcher.score:,} | High: {watcher.high_score:,} | FPS: {fps_text}"
639
+ )
640
+ except KeyboardInterrupt:
641
+ print("\n\n⏹️ Stopped by user")
642
+
643
+ pygame.quit()
644
+ print(f"\n📊 Session Stats:")
645
+ print(f" Games Played: {watcher.games_played}")
646
+ print(f" High Score: {watcher.high_score}")
647
+
648
+
649
+ if __name__ == "__main__":
650
+ main()