Upload 28 files
Browse files- app.py +239 -0
- best_brain.pkl +3 -0
- neurodino/web-ui/canvas.html +4 -0
- neurodino/web-ui/custom_controls.html +364 -0
- neurodino/web-ui/style.css +0 -0
- neurodino/web-ui/visualizer.js +1036 -0
- pydino/assets/100-offline-sprite.png +0 -0
- pydino/assets/200-offline-sprite.png +0 -0
- pydino/background_el.py +195 -0
- pydino/cloud.py +120 -0
- pydino/constants.py +33 -0
- pydino/debug_overlay.py +65 -0
- pydino/dimensions.py +8 -0
- pydino/distance_meter.py +302 -0
- pydino/game_over_panel.py +272 -0
- pydino/horizon.py +352 -0
- pydino/horizon_line.py +114 -0
- pydino/image_sprite_provider.py +80 -0
- pydino/night_mode.py +149 -0
- pydino/obstacle.py +294 -0
- pydino/offline_sprite_definitions.py +245 -0
- pydino/runner.py +766 -0
- pydino/sounds/button-press.mp3 +0 -0
- pydino/sounds/hit.mp3 +0 -0
- pydino/sounds/score-reached.mp3 +0 -0
- pydino/trex.py +488 -0
- pydino/utils.py +32 -0
- watch_model.py +650 -0
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()
|