crystalwindow 4.4__tar.gz → 4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crystalwindow-4.4/crystalwindow.egg-info → crystalwindow-4.6}/PKG-INFO +1 -1
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/__init__.py +17 -4
- crystalwindow-4.6/crystalwindow/ai.py +314 -0
- crystalwindow-4.6/crystalwindow/clock.py +189 -0
- crystalwindow-4.6/crystalwindow/cw_client.py +95 -0
- crystalwindow-4.6/crystalwindow/websearch.py +233 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/window.py +6 -1
- {crystalwindow-4.4 → crystalwindow-4.6/crystalwindow.egg-info}/PKG-INFO +1 -1
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/SOURCES.txt +2 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/setup.py +1 -1
- crystalwindow-4.4/crystalwindow/ai.py +0 -366
- crystalwindow-4.4/crystalwindow/clock.py +0 -23
- {crystalwindow-4.4 → crystalwindow-4.6}/LICENSE +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/MANIFEST.in +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/README.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/FileHelper.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/Icons/default_icon.png +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/animation.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/assets.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/camera.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/collision.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/color_handler.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/crystal3d.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/docs/getting_started.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/docs/index.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_helpers.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_rects.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_text_helper.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/draw_tool.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/fun_helpers.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/3dsquare.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/__init__.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/__main__.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/gravitytest.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/guitesting.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/sandbox.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/squaremove.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gametests/windowtesting.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gravity.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gui.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/gui_ext.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/math.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/player.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/sprites.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/tilemap.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow/ver_warner.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/dependency_links.txt +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/crystalwindow.egg-info/top_level.txt +0 -0
- {crystalwindow-4.4 → crystalwindow-4.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crystalwindow
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6
|
|
4
4
|
Summary: A Tkinter powered window + GUI toolkit made by Crystal (MEEEEEE)! Easier apps, smoother UI and all-in-one helpers!
|
|
5
5
|
Home-page: https://pypi.org/project/crystalwindow/
|
|
6
6
|
Author: CrystalBallyHereXD
|
|
@@ -32,8 +32,9 @@ from .collision import check_collision, resolve_collision
|
|
|
32
32
|
from .gui import Button, Label, GUIManager, hex_to_rgb, Fade
|
|
33
33
|
from .gui_ext import Toggle, Slider
|
|
34
34
|
|
|
35
|
-
# === Time ===
|
|
36
|
-
|
|
35
|
+
# === Time System ===
|
|
36
|
+
# Includes upgraded Clock + all helper utilities
|
|
37
|
+
from .clock import Clock, Stopwatch, CountdownTimer, Scheduler
|
|
37
38
|
|
|
38
39
|
# === Drawing Helpers ===
|
|
39
40
|
from .draw_helpers import gradient_rect, CameraShake
|
|
@@ -45,6 +46,7 @@ from .draw_tool import CrystalDraw
|
|
|
45
46
|
from .fun_helpers import random_name, DebugOverlay, random_color, random_palette, lerp
|
|
46
47
|
from .camera import Camera
|
|
47
48
|
from .color_handler import Colors, Color
|
|
49
|
+
from .websearch import WebSearchResult, WebSearch
|
|
48
50
|
|
|
49
51
|
# === 3D Engine ===
|
|
50
52
|
from .crystal3d import CW3D
|
|
@@ -52,6 +54,9 @@ from .crystal3d import CW3D
|
|
|
52
54
|
# === AI Engine ===
|
|
53
55
|
from .ai import AI
|
|
54
56
|
|
|
57
|
+
# === Server Manager ===
|
|
58
|
+
from .cw_client import CWClient
|
|
59
|
+
|
|
55
60
|
__all__ = [
|
|
56
61
|
# --- Core ---
|
|
57
62
|
"Window", "Sprite", "TileMap", "Player", "Gravity", "FileHelper", "Mathematics",
|
|
@@ -77,18 +82,26 @@ __all__ = [
|
|
|
77
82
|
# --- GUI Extensions ---
|
|
78
83
|
"Toggle", "Slider",
|
|
79
84
|
|
|
80
|
-
# --- Time ---
|
|
85
|
+
# --- Time System ---
|
|
86
|
+
# FULL time system is now exposed
|
|
81
87
|
"Clock",
|
|
88
|
+
"Stopwatch",
|
|
89
|
+
"CountdownTimer",
|
|
90
|
+
"Scheduler",
|
|
82
91
|
|
|
83
92
|
# --- Drawing ---
|
|
84
93
|
"gradient_rect", "CameraShake", "DrawHelper", "DrawTextManager", "CrystalDraw",
|
|
85
94
|
|
|
86
95
|
# --- Misc ---
|
|
87
|
-
"random_name", "DebugOverlay", "Camera", "Colors", "Color",
|
|
96
|
+
"random_name", "DebugOverlay", "Camera", "Colors", "Color",
|
|
97
|
+
"random_palette", "lerp", "WebSearchResult", "WebSearch",
|
|
88
98
|
|
|
89
99
|
# --- 3D ---
|
|
90
100
|
"CW3D",
|
|
91
101
|
|
|
92
102
|
# --- AI ---
|
|
93
103
|
"AI",
|
|
104
|
+
|
|
105
|
+
# --- Server Client ---
|
|
106
|
+
"CWClient",
|
|
94
107
|
]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# ==========================================================
|
|
2
|
+
# CrystalAI v0.7 — Stabilized & Corrected Engine
|
|
3
|
+
# ----------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import ast
|
|
7
|
+
import difflib
|
|
8
|
+
import requests
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ==========================================================
|
|
14
|
+
# Response Wrapper
|
|
15
|
+
# ==========================================================
|
|
16
|
+
class CrystalAIResponse:
|
|
17
|
+
def __init__(self, text: str, meta: Optional[Dict[str, Any]] = None):
|
|
18
|
+
self.text = text
|
|
19
|
+
self.meta = meta or {}
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
return self.text
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ==========================================================
|
|
26
|
+
# MAIN ENGINE
|
|
27
|
+
# ==========================================================
|
|
28
|
+
class AI:
|
|
29
|
+
DEFAULT_MODEL = "llama-3.1-70b-versatile"
|
|
30
|
+
DEFAULT_PERSONALITY = (
|
|
31
|
+
"You are CrystalWindow AI. You help users with Python code, "
|
|
32
|
+
"debugging, error analysis, documentation, and file analysis. "
|
|
33
|
+
"Be friendly, technical, clear, and precise."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------
|
|
39
|
+
def __init__(self, key=None, model=None):
|
|
40
|
+
|
|
41
|
+
# --- KEY VALIDATION ---
|
|
42
|
+
# If no key passed try common environment variables (makes key optional)
|
|
43
|
+
if not key:
|
|
44
|
+
key = (
|
|
45
|
+
os.environ.get("CRYSTALAI_API_KEY")
|
|
46
|
+
or os.environ.get("GROQ_API_KEY")
|
|
47
|
+
or os.environ.get("OPENAI_API_KEY")
|
|
48
|
+
or os.environ.get("API_KEY")
|
|
49
|
+
)
|
|
50
|
+
if key:
|
|
51
|
+
key = str(key).strip()
|
|
52
|
+
|
|
53
|
+
if not key or len(str(key).strip()) == 0:
|
|
54
|
+
print("[CrystalAI] No API key provided → using offline mode.")
|
|
55
|
+
self.key = None # forces offline fallback
|
|
56
|
+
self.force_local = True
|
|
57
|
+
else:
|
|
58
|
+
self.key = key
|
|
59
|
+
self.force_local = False
|
|
60
|
+
|
|
61
|
+
# --- MODEL VALIDATION ---
|
|
62
|
+
# If model is omitted (None) use default silently; only warn if an invalid model is explicitly passed
|
|
63
|
+
if model is None:
|
|
64
|
+
self.model = self.DEFAULT_MODEL
|
|
65
|
+
elif not isinstance(model, str) or len(model) < 3:
|
|
66
|
+
print("[CrystalAI] Unknown model → using default.")
|
|
67
|
+
self.model = self.DEFAULT_MODEL
|
|
68
|
+
else:
|
|
69
|
+
self.model = model
|
|
70
|
+
|
|
71
|
+
self.personality = self.DEFAULT_PERSONALITY
|
|
72
|
+
self.memory = []
|
|
73
|
+
self.use_memory = True
|
|
74
|
+
self.library_context = ""
|
|
75
|
+
|
|
76
|
+
# ==========================================================
|
|
77
|
+
# PERSONALITY
|
|
78
|
+
# ==========================================================
|
|
79
|
+
def set_personality(self, txt):
|
|
80
|
+
if not isinstance(txt, str) or len(txt.strip()) < 10:
|
|
81
|
+
print("[CrystalAI] Personality too short → reverting to default.")
|
|
82
|
+
self.personality = self.DEFAULT_PERSONALITY
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if len(txt) > 3000:
|
|
86
|
+
print("[CrystalAI] Personality too long → using default.")
|
|
87
|
+
self.personality = self.DEFAULT_PERSONALITY
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.personality = txt.strip()
|
|
91
|
+
|
|
92
|
+
# ==========================================================
|
|
93
|
+
# LIBRARY INGESTION
|
|
94
|
+
# ==========================================================
|
|
95
|
+
def index_library(self, folder):
|
|
96
|
+
"""
|
|
97
|
+
Load all Python files as context for smarter answers.
|
|
98
|
+
"""
|
|
99
|
+
if not os.path.exists(folder):
|
|
100
|
+
print("[CrystalAI] Library folder not found.")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
collected = []
|
|
104
|
+
for root, _, files in os.walk(folder):
|
|
105
|
+
for f in files:
|
|
106
|
+
if f.endswith(".py"):
|
|
107
|
+
try:
|
|
108
|
+
p = os.path.join(root, f)
|
|
109
|
+
with open(p, "r", encoding="utf8") as fp:
|
|
110
|
+
collected.append(
|
|
111
|
+
f"# FILE: {p}\n{fp.read()}\n\n"
|
|
112
|
+
)
|
|
113
|
+
except:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
self.library_context = "\n".join(collected)[:120_000] # trimmed
|
|
117
|
+
|
|
118
|
+
# ==========================================================
|
|
119
|
+
# FILE READER
|
|
120
|
+
# ==========================================================
|
|
121
|
+
def _read_file(self, path):
|
|
122
|
+
if not path:
|
|
123
|
+
return None
|
|
124
|
+
if not os.path.exists(path):
|
|
125
|
+
return f"[CrystalAI] file not found: {path}"
|
|
126
|
+
try:
|
|
127
|
+
with open(path, "r", encoding="utf8") as f:
|
|
128
|
+
return f.read()
|
|
129
|
+
except:
|
|
130
|
+
return "[CrystalAI] could not read file."
|
|
131
|
+
|
|
132
|
+
# ==========================================================
|
|
133
|
+
# PROMPT BUILDER
|
|
134
|
+
# ==========================================================
|
|
135
|
+
def _build_prompt(self, user_text, file_data):
|
|
136
|
+
final = (
|
|
137
|
+
f"[SYSTEM]\n{self.personality}\n\n"
|
|
138
|
+
f"[USER]\n{user_text}\n\n"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if self.use_memory and self.memory:
|
|
142
|
+
final += "[MEMORY]\n"
|
|
143
|
+
for m in self.memory[-6:]:
|
|
144
|
+
final += f"User: {m['user']}\nAI: {m['ai']}\n"
|
|
145
|
+
final += "\n"
|
|
146
|
+
|
|
147
|
+
if self.library_context:
|
|
148
|
+
final += f"[LIBRARY]\n{self.library_context}\n\n"
|
|
149
|
+
|
|
150
|
+
if file_data and not file_data.startswith("[CrystalAI]"):
|
|
151
|
+
final += f"[FILE]\n{file_data}\n\n"
|
|
152
|
+
|
|
153
|
+
return final[:190_000] # safety limit
|
|
154
|
+
|
|
155
|
+
def _save_memory(self, user, ai):
|
|
156
|
+
self.memory.append({"user": user, "ai": ai})
|
|
157
|
+
if len(self.memory) > 60:
|
|
158
|
+
self.memory.pop(0)
|
|
159
|
+
|
|
160
|
+
# ==========================================================
|
|
161
|
+
# LOCAL FALLBACK AI
|
|
162
|
+
# ==========================================================
|
|
163
|
+
def _local_ai(self, user_text, file_data):
|
|
164
|
+
"""
|
|
165
|
+
Much cleaner, more reliable fallback engine.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
# --- AST SECTION ---
|
|
169
|
+
if file_data and not file_data.startswith("[CrystalAI]"):
|
|
170
|
+
try:
|
|
171
|
+
ast.parse(file_data)
|
|
172
|
+
return (
|
|
173
|
+
"[LocalAI] File parsed successfully — no syntax errors.\n"
|
|
174
|
+
"Ask for refactoring, explanation, or improvements."
|
|
175
|
+
)
|
|
176
|
+
except SyntaxError as se:
|
|
177
|
+
snippet = self._snippet(file_data, se.lineno)
|
|
178
|
+
return (
|
|
179
|
+
"[LocalAI] SyntaxError found:\n"
|
|
180
|
+
f"• {se.msg}\n"
|
|
181
|
+
f"• Line {se.lineno}\n\n"
|
|
182
|
+
f"{snippet}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# --- GENERIC HELP ---
|
|
186
|
+
lower = user_text.lower()
|
|
187
|
+
if "python" in lower or "fix" in lower or "error" in lower:
|
|
188
|
+
return (
|
|
189
|
+
"[LocalAI] Offline mode: I can help with general Python logic.\n"
|
|
190
|
+
"If you provide a file, I can analyze it using AST."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return "[LocalAI] Offline mode active — limited responses available."
|
|
194
|
+
|
|
195
|
+
# ==========================================================
|
|
196
|
+
# MAIN "ASK" FUNCTION
|
|
197
|
+
# ==========================================================
|
|
198
|
+
def ask(self, text, file=None):
|
|
199
|
+
file_data = self._read_file(file)
|
|
200
|
+
prompt = self._build_prompt(text, file_data)
|
|
201
|
+
|
|
202
|
+
# If no API key → offline only
|
|
203
|
+
if self.force_local:
|
|
204
|
+
resp = self._local_ai(text, file_data)
|
|
205
|
+
self._save_memory(text, resp)
|
|
206
|
+
return CrystalAIResponse(resp)
|
|
207
|
+
|
|
208
|
+
# Online mode
|
|
209
|
+
try:
|
|
210
|
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
|
211
|
+
headers = {
|
|
212
|
+
"Authorization": f"Bearer {self.key}",
|
|
213
|
+
"Content-Type": "application/json"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
payload = {
|
|
217
|
+
"model": self.model,
|
|
218
|
+
"messages": [
|
|
219
|
+
{"role": "system", "content": self.personality},
|
|
220
|
+
{"role": "user", "content": prompt}
|
|
221
|
+
],
|
|
222
|
+
"temperature": 0.3
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
r = requests.post(url, json=payload, headers=headers, timeout=8)
|
|
226
|
+
data = r.json()
|
|
227
|
+
|
|
228
|
+
if "error" in data:
|
|
229
|
+
raise RuntimeError(data["error"])
|
|
230
|
+
|
|
231
|
+
resp = data["choices"][0]["message"]["content"]
|
|
232
|
+
|
|
233
|
+
except Exception:
|
|
234
|
+
resp = self._local_ai(text, file_data)
|
|
235
|
+
|
|
236
|
+
self._save_memory(text, resp)
|
|
237
|
+
return CrystalAIResponse(resp)
|
|
238
|
+
|
|
239
|
+
# ==========================================================
|
|
240
|
+
# TERMINAL HELPER
|
|
241
|
+
# ==========================================================
|
|
242
|
+
def ask_t(self, text, file=None):
|
|
243
|
+
return self.ask(f"[TERMINAL] {text}", file)
|
|
244
|
+
|
|
245
|
+
# ==========================================================
|
|
246
|
+
# AUTO FIX CODE
|
|
247
|
+
# ==========================================================
|
|
248
|
+
def fix_code(self, file_path):
|
|
249
|
+
orig = self._read_file(file_path)
|
|
250
|
+
|
|
251
|
+
if not orig or orig.startswith("[CrystalAI]"):
|
|
252
|
+
return CrystalAIResponse(orig or "[CrystalAI] file missing")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
ast.parse(orig)
|
|
256
|
+
return CrystalAIResponse("[AI] No syntax errors found.")
|
|
257
|
+
except SyntaxError as se:
|
|
258
|
+
fixed, notes = self._simple_fix(orig, se)
|
|
259
|
+
diff = self._make_diff(orig, fixed)
|
|
260
|
+
msg = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
|
|
261
|
+
return CrystalAIResponse(msg, {"diff": diff, "notes": notes})
|
|
262
|
+
|
|
263
|
+
# ==========================================================
|
|
264
|
+
# SIMPLE AUTO-FIX ENGINE
|
|
265
|
+
# ==========================================================
|
|
266
|
+
def _simple_fix(self, src, err):
|
|
267
|
+
notes = []
|
|
268
|
+
lines = src.splitlines()
|
|
269
|
+
msg = getattr(err, "msg", "")
|
|
270
|
+
lineno = err.lineno or 0
|
|
271
|
+
|
|
272
|
+
if "expected" in msg and ":" in msg:
|
|
273
|
+
if 1 <= lineno <= len(lines):
|
|
274
|
+
l = lines[lineno - 1].rstrip()
|
|
275
|
+
if not l.endswith(":"):
|
|
276
|
+
lines[lineno - 1] = l + ":"
|
|
277
|
+
notes.append("[fix] added missing ':'")
|
|
278
|
+
candidate = "\n".join(lines)
|
|
279
|
+
try:
|
|
280
|
+
ast.parse(candidate)
|
|
281
|
+
return candidate, notes
|
|
282
|
+
except:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
notes.append("[info] auto-fix could not fix the error")
|
|
286
|
+
return src, notes
|
|
287
|
+
|
|
288
|
+
# ==========================================================
|
|
289
|
+
# DIFF HELPER
|
|
290
|
+
# ==========================================================
|
|
291
|
+
def _make_diff(self, old, new):
|
|
292
|
+
return "\n".join(
|
|
293
|
+
difflib.unified_diff(
|
|
294
|
+
old.splitlines(), new.splitlines(),
|
|
295
|
+
fromfile="old", tofile="new", lineterm=""
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# ==========================================================
|
|
300
|
+
# SNIPPET HELPER
|
|
301
|
+
# ==========================================================
|
|
302
|
+
def _snippet(self, src, lineno, ctx=2):
|
|
303
|
+
lines = src.splitlines()
|
|
304
|
+
start = max(0, lineno - ctx - 1)
|
|
305
|
+
end = min(len(lines), lineno + ctx)
|
|
306
|
+
out = []
|
|
307
|
+
for i in range(start, end):
|
|
308
|
+
mark = "->" if (i + 1) == lineno else " "
|
|
309
|
+
out.append(f"{mark} {i+1:4}: {lines[i]}")
|
|
310
|
+
return "\n".join(out)
|
|
311
|
+
|
|
312
|
+
# ==========================================================
|
|
313
|
+
# END OF ENGINE
|
|
314
|
+
# ==========================================================
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from collections import deque
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ============================================================
|
|
7
|
+
# Stopwatch Helper
|
|
8
|
+
# ============================================================
|
|
9
|
+
class Stopwatch:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.start_time = None
|
|
12
|
+
self.elapsed_time = 0.0
|
|
13
|
+
self.running = False
|
|
14
|
+
|
|
15
|
+
def start(self):
|
|
16
|
+
if not self.running:
|
|
17
|
+
self.start_time = time.perf_counter()
|
|
18
|
+
self.running = True
|
|
19
|
+
|
|
20
|
+
def stop(self):
|
|
21
|
+
if self.running:
|
|
22
|
+
self.elapsed_time += time.perf_counter() - self.start_time
|
|
23
|
+
self.running = False
|
|
24
|
+
|
|
25
|
+
def reset(self):
|
|
26
|
+
self.start_time = None
|
|
27
|
+
self.elapsed_time = 0.0
|
|
28
|
+
self.running = False
|
|
29
|
+
|
|
30
|
+
def elapsed(self):
|
|
31
|
+
if self.running:
|
|
32
|
+
return self.elapsed_time + (time.perf_counter() - self.start_time)
|
|
33
|
+
return self.elapsed_time
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ============================================================
|
|
37
|
+
# Countdown Timer Helper
|
|
38
|
+
# ============================================================
|
|
39
|
+
class CountdownTimer:
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.target_time = None
|
|
42
|
+
|
|
43
|
+
def start(self, seconds: float):
|
|
44
|
+
self.target_time = time.perf_counter() + seconds
|
|
45
|
+
|
|
46
|
+
def remaining(self):
|
|
47
|
+
if not self.target_time:
|
|
48
|
+
return 0
|
|
49
|
+
return max(0, self.target_time - time.perf_counter())
|
|
50
|
+
|
|
51
|
+
def done(self):
|
|
52
|
+
return self.remaining() == 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ============================================================
|
|
56
|
+
# Event Scheduler Helper
|
|
57
|
+
# ============================================================
|
|
58
|
+
class Scheduler:
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.events = [] # list of (interval, last_run, function)
|
|
61
|
+
|
|
62
|
+
def schedule(self, interval: float, func):
|
|
63
|
+
"""Run function every `interval` seconds."""
|
|
64
|
+
self.events.append([interval, time.perf_counter(), func])
|
|
65
|
+
|
|
66
|
+
def run_pending(self):
|
|
67
|
+
now = time.perf_counter()
|
|
68
|
+
for event in self.events:
|
|
69
|
+
interval, last_run, func = event
|
|
70
|
+
if now - last_run >= interval:
|
|
71
|
+
func()
|
|
72
|
+
event[1] = now # update last_run
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ============================================================
|
|
76
|
+
# MAIN CLOCK
|
|
77
|
+
# ============================================================
|
|
78
|
+
class Clock:
|
|
79
|
+
def __init__(self, target_fps: int = 60, smooth_fps: int = 30):
|
|
80
|
+
self.last = time.perf_counter()
|
|
81
|
+
self.delta = 0.0
|
|
82
|
+
|
|
83
|
+
# FPS
|
|
84
|
+
self.target_fps = target_fps
|
|
85
|
+
self.min_frame_time = 1 / target_fps if target_fps else 0
|
|
86
|
+
self.frame_times = deque(maxlen=smooth_fps)
|
|
87
|
+
|
|
88
|
+
self.paused = False
|
|
89
|
+
|
|
90
|
+
# Utilities
|
|
91
|
+
self.stopwatch = Stopwatch()
|
|
92
|
+
self.timer = CountdownTimer()
|
|
93
|
+
self.scheduler = Scheduler()
|
|
94
|
+
|
|
95
|
+
# ----------------------------------------------------------
|
|
96
|
+
# TICK + FPS
|
|
97
|
+
# ----------------------------------------------------------
|
|
98
|
+
def tick(self, fps: int | None = None):
|
|
99
|
+
if fps:
|
|
100
|
+
self.target_fps = fps
|
|
101
|
+
self.min_frame_time = 1 / fps
|
|
102
|
+
|
|
103
|
+
now = time.perf_counter()
|
|
104
|
+
raw_delta = now - self.last
|
|
105
|
+
self.last = now
|
|
106
|
+
|
|
107
|
+
# Paused = no delta
|
|
108
|
+
if self.paused:
|
|
109
|
+
self.delta = 0.0
|
|
110
|
+
return 0.0
|
|
111
|
+
|
|
112
|
+
# FPS limiting
|
|
113
|
+
sleep_time = self.min_frame_time - raw_delta
|
|
114
|
+
if sleep_time > 0:
|
|
115
|
+
time.sleep(sleep_time)
|
|
116
|
+
now2 = time.perf_counter()
|
|
117
|
+
raw_delta = now2 - (self.last - raw_delta)
|
|
118
|
+
|
|
119
|
+
self.delta = raw_delta
|
|
120
|
+
self.frame_times.append(raw_delta)
|
|
121
|
+
|
|
122
|
+
# Scheduler update
|
|
123
|
+
self.scheduler.run_pending()
|
|
124
|
+
|
|
125
|
+
return self.delta
|
|
126
|
+
|
|
127
|
+
def get_fps(self):
|
|
128
|
+
if not self.frame_times:
|
|
129
|
+
return 0.0
|
|
130
|
+
avg = sum(self.frame_times) / len(self.frame_times)
|
|
131
|
+
return round(1 / avg, 2) if avg > 0 else 0
|
|
132
|
+
|
|
133
|
+
# ----------------------------------------------------------
|
|
134
|
+
# PAUSE / RESUME
|
|
135
|
+
# ----------------------------------------------------------
|
|
136
|
+
def pause(self):
|
|
137
|
+
self.paused = True
|
|
138
|
+
|
|
139
|
+
def resume(self):
|
|
140
|
+
self.paused = False
|
|
141
|
+
self.last = time.perf_counter()
|
|
142
|
+
|
|
143
|
+
# ----------------------------------------------------------
|
|
144
|
+
# TIME + DATE UTILITIES
|
|
145
|
+
# ----------------------------------------------------------
|
|
146
|
+
def time(self, milliseconds: bool = True, hour_12: bool = False):
|
|
147
|
+
now = datetime.now()
|
|
148
|
+
|
|
149
|
+
if hour_12:
|
|
150
|
+
fmt = "%I:%M:%S" + (" %p" if not milliseconds else ".%f %p")
|
|
151
|
+
else:
|
|
152
|
+
fmt = "%H:%M:%S" + (".%f" if milliseconds else "")
|
|
153
|
+
|
|
154
|
+
text = now.strftime(fmt)
|
|
155
|
+
if milliseconds:
|
|
156
|
+
# Trim microseconds to milliseconds
|
|
157
|
+
if hour_12:
|
|
158
|
+
# Example: "03:20:15.123000 PM" -> "03:20:15.123 PM"
|
|
159
|
+
parts = text.split(".")
|
|
160
|
+
ms = parts[1][:3] + parts[1][6:] # keep .mmm, remove "000 PM"
|
|
161
|
+
text = parts[0] + "." + parts[1][:3] + " " + now.strftime("%p")
|
|
162
|
+
else:
|
|
163
|
+
text = text[:-3] # remove last 3 microseconds digits
|
|
164
|
+
|
|
165
|
+
return text
|
|
166
|
+
|
|
167
|
+
def date(self, format: str = "%m/%d/%Y"):
|
|
168
|
+
return datetime.now().strftime(format)
|
|
169
|
+
|
|
170
|
+
def time_date(self, order: str = "time_first", **kwargs):
|
|
171
|
+
t = self.time(**kwargs)
|
|
172
|
+
d = self.date()
|
|
173
|
+
return f"{t} | {d}" if order == "time_first" else f"{d} | {t}"
|
|
174
|
+
|
|
175
|
+
# ----------------------------------------------------------
|
|
176
|
+
# CUSTOM FORMATTERS
|
|
177
|
+
# ----------------------------------------------------------
|
|
178
|
+
def format_time(self, fmt: str):
|
|
179
|
+
return datetime.now().strftime(fmt)
|
|
180
|
+
|
|
181
|
+
def format_date(self, fmt: str):
|
|
182
|
+
return datetime.now().strftime(fmt)
|
|
183
|
+
|
|
184
|
+
# ----------------------------------------------------------
|
|
185
|
+
# MONOTONIC TIMESTAMP
|
|
186
|
+
# ----------------------------------------------------------
|
|
187
|
+
def timestamp(self):
|
|
188
|
+
"""High-precision monotonic timestamp."""
|
|
189
|
+
return time.perf_counter()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# cw_client.py
|
|
2
|
+
# pip install websockets
|
|
3
|
+
import asyncio
|
|
4
|
+
import websockets
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
class CWClient:
|
|
10
|
+
"""
|
|
11
|
+
Lightweight client wrapper. Usage:
|
|
12
|
+
c = CWClient("MyName", "ws://host:8765")
|
|
13
|
+
c.on_event = lambda ev: print("ev", ev)
|
|
14
|
+
c.start()
|
|
15
|
+
c.move(x,y)
|
|
16
|
+
c.chat("hi")
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, pid, uri="ws://127.0.0.1:8765"):
|
|
19
|
+
self.pid = pid
|
|
20
|
+
self.uri = uri
|
|
21
|
+
self._send_q = asyncio.Queue()
|
|
22
|
+
self.on_event = None # callback for incoming events: fn(dict)
|
|
23
|
+
self._running = False
|
|
24
|
+
self._loop_thread = None
|
|
25
|
+
|
|
26
|
+
async def _run(self):
|
|
27
|
+
try:
|
|
28
|
+
async with websockets.connect(self.uri) as ws:
|
|
29
|
+
# send join
|
|
30
|
+
await ws.send(json.dumps({"type":"join","id":self.pid}))
|
|
31
|
+
self._running = True
|
|
32
|
+
sender_task = asyncio.create_task(self._sender(ws))
|
|
33
|
+
try:
|
|
34
|
+
async for raw in ws:
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(raw)
|
|
37
|
+
except:
|
|
38
|
+
continue
|
|
39
|
+
if self.on_event:
|
|
40
|
+
# run callback in same thread (safe)
|
|
41
|
+
try:
|
|
42
|
+
self.on_event(data)
|
|
43
|
+
except Exception as cb_e:
|
|
44
|
+
# swallow callback errors
|
|
45
|
+
print("on_event callback error:", cb_e)
|
|
46
|
+
finally:
|
|
47
|
+
sender_task.cancel()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
# notify user on connection lost
|
|
50
|
+
if self.on_event:
|
|
51
|
+
try:
|
|
52
|
+
self.on_event({"type":"sys","msg":f"connection_lost:{e}"})
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
finally:
|
|
56
|
+
self._running = False
|
|
57
|
+
|
|
58
|
+
async def _sender(self, ws):
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
obj = await self._send_q.get()
|
|
62
|
+
await ws.send(json.dumps(obj))
|
|
63
|
+
except Exception:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
def start(self):
|
|
67
|
+
if self._loop_thread and self._loop_thread.is_alive():
|
|
68
|
+
return
|
|
69
|
+
def target():
|
|
70
|
+
asyncio.run(self._run())
|
|
71
|
+
self._loop_thread = threading.Thread(target=target, daemon=True)
|
|
72
|
+
self._loop_thread.start()
|
|
73
|
+
# give a bit of time to connect
|
|
74
|
+
time.sleep(0.05)
|
|
75
|
+
|
|
76
|
+
def send(self, obj):
|
|
77
|
+
# enqueue safely from any thread
|
|
78
|
+
async def push():
|
|
79
|
+
await self._send_q.put(obj)
|
|
80
|
+
try:
|
|
81
|
+
loop = asyncio.get_running_loop()
|
|
82
|
+
# if main thread has a loop, schedule
|
|
83
|
+
asyncio.run_coroutine_threadsafe(push(), loop)
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
# no running loop in this thread: create a tiny loop to push
|
|
86
|
+
def run_push():
|
|
87
|
+
asyncio.run(push())
|
|
88
|
+
threading.Thread(target=run_push, daemon=True).start()
|
|
89
|
+
|
|
90
|
+
# convenience APIs
|
|
91
|
+
def move(self, x, y):
|
|
92
|
+
self.send({"type":"move","id":self.pid,"x":int(x),"y":int(y)})
|
|
93
|
+
|
|
94
|
+
def chat(self, msg):
|
|
95
|
+
self.send({"type":"chat","id":self.pid,"msg":str(msg)})
|