arcadeturtle 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,33 @@
1
+ """ArcadeTurtle — همان API ماژول turtle، روی موتور Arcade."""
2
+
3
+ from ._errors import ArcadeTurtleError, Terminator
4
+ from .api import * # noqa: F401,F403 — API تابعی سراسری
5
+ from .api import ( # صریح، برای ابزارهای تحلیل کد
6
+ addshape, backward, begin_fill, begin_poly, bgcolor, bgpic, bk, back,
7
+ bye, circle, clear, clearscreen, clearstamp, clearstamps, clone, color,
8
+ colormode, degrees, delay, distance, done, dot, down, end_fill,
9
+ end_poly, exitonclick, fd, fillcolor, filling, forward, getcanvas,
10
+ getpen, get_poly, get_shapepoly, getshapes, getturtle, goto, heading,
11
+ hideturtle, home, ht, isdown, isvisible, left, lt, mainloop, mode,
12
+ pen, pencolor, pendown, pensize, penup, pd, pos, position, pu,
13
+ radians, register_shape,
14
+ reset, resetscreen, resizemode, right, rt, screensize, seth, setheading,
15
+ setpos, setposition, settiltangle, setup, setworldcoordinates, setx,
16
+ sety, shapetransform, shearfactor, teleport, tilt, tiltangle, listen,
17
+ onclick, ondrag, onkey, onkeypress, onkeyrelease, onrelease,
18
+ onscreenclick, ontimer, shape,
19
+ shapesize, showturtle, speed, st, stamp, textinput, title, towards,
20
+ tracer, turtles, turtlesize, undo, undobufferentries, setundobuffer, up,
21
+ update, numinput, width, window_height, window_width, write,
22
+ write_docstringdict, xcor, ycor,
23
+ )
24
+ from .screen import Screen, ScrolledCanvas, TurtleScreen
25
+ from .turtle_obj import Pen, RawPen, RawTurtle, Turtle
26
+ from ._shapes import Shape
27
+ from ._vec2d import Vec2D
28
+
29
+
30
+ def getscreen():
31
+ return Screen()
32
+
33
+ __version__ = "0.1.0"
@@ -0,0 +1,45 @@
1
+ """فرمان‌های صف — واحدهای پخش انیمیشن در Engine.
2
+
3
+ هر فراخوانی API کاربر در حالت RECORDING یکی از این‌ها را به صف سراسری اضافه می‌کند.
4
+ ترتیب صف = ترتیب فراخوانی برنامه (یک صف واحد برای همه لاک‌پشت‌ها).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Callable
11
+
12
+ Point = tuple[float, float]
13
+ Color = tuple[int, int, int]
14
+
15
+
16
+ @dataclass
17
+ class MoveCmd:
18
+ """حرکت خطی (forward/backward/goto). انیمیشن با سرعت خطی."""
19
+ turtle_id: int
20
+ start: Point
21
+ end: Point
22
+ pen_down: bool
23
+ color: Color
24
+ size: int
25
+ speed: int # سرعت لاک‌پشت در لحظه ثبت فرمان
26
+
27
+
28
+ @dataclass
29
+ class RotateCmd:
30
+ """چرخش (left/right/setheading). انیمیشن با سرعت زاویه‌ای."""
31
+ turtle_id: int
32
+ start_heading: float
33
+ end_heading: float
34
+ delta: float # علامت‌دار: مثبت = پادساعتگرد؛ اندازه واقعی چرخش
35
+ speed: int
36
+
37
+
38
+ @dataclass
39
+ class InstantCmd:
40
+ """فرمان بدون انیمیشن (penup، رنگ، dot، write، ...).
41
+
42
+ fn وقتی نوبتش در صف رسید اجرا می‌شود تا ترتیب دیداری حفظ شود.
43
+ """
44
+ turtle_id: int
45
+ fn: Callable[[], None]
@@ -0,0 +1,46 @@
1
+ """تبدیل مختصات و زاویه بین دنیای Turtle و دنیای Arcade.
2
+
3
+ قرارداد Turtle: مبدأ وسط صفحه، y به بالا، زاویه صفر = شرق، درجه پادساعتگرد.
4
+ قرارداد Arcade: مبدأ گوشه پایین-چپ، y به بالا.
5
+
6
+ هیچ‌جای دیگر پروژه نباید width/2 یا height/2 اضافه کند — فقط اینجا.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ # مختصات جهانی دلخواه (setworldcoordinates): (llx, lly, urx, ury) یا None
12
+ # برای حالت پیش‌فرض. وقتی فعال است، این مستطیل به کل پنجره کشیده می‌شود —
13
+ # اگر نسبت x/y آن با نسبت پنجره یکی نباشد، زاویه‌ها کج‌نما می‌شوند؛ همان
14
+ # چیزی که خودِ turtle استاندارد هم صراحتاً هشدار می‌دهد.
15
+ World = tuple[float, float, float, float]
16
+
17
+
18
+ def to_arcade(tx: float, ty: float, width: int, height: int,
19
+ world: World | None = None) -> tuple[float, float]:
20
+ if world is None:
21
+ return tx + width / 2, ty + height / 2
22
+ llx, lly, urx, ury = world
23
+ ax = (tx - llx) / (urx - llx) * width
24
+ ay = (ty - lly) / (ury - lly) * height
25
+ return ax, ay
26
+
27
+
28
+ def to_turtle(ax: float, ay: float, width: int, height: int,
29
+ world: World | None = None) -> tuple[float, float]:
30
+ if world is None:
31
+ return ax - width / 2, ay - height / 2
32
+ llx, lly, urx, ury = world
33
+ tx = ax / width * (urx - llx) + llx
34
+ ty = ay / height * (ury - lly) + lly
35
+ return tx, ty
36
+
37
+
38
+ def heading_to_arcade_angle(heading: float) -> float:
39
+ """زاویه turtle (پادساعتگرد از شرق) → زاویه دوران چندضلعی کرسر.
40
+
41
+ چندضلعی‌های کرسر در _renderer «رو به شرق» تعریف شده‌اند و با ماتریس دوران
42
+ استاندارد (پادساعتگرد) چرخانده می‌شوند؛ پس heading مستقیم قابل استفاده است.
43
+ اگر روزی Sprite تصویری اضافه شد (Sprite.angle در Arcade 3 ساعتگرد است)،
44
+ تبدیلش فقط همین‌جا اضافه می‌شود.
45
+ """
46
+ return heading
@@ -0,0 +1,285 @@
1
+ """Engine — قلب کتابخانه: صف فرمان، پخش انیمیشن، مدیریت پنجره و لاک‌پشت‌ها.
2
+
3
+ مدل اجرا (تصمیم ۱ پلن، ساده‌شده):
4
+ - state «منطقی» هر لاک‌پشت همان لحظه‌ی فراخوانی API آپدیت می‌شود تا
5
+ کوئری‌هایی مثل position()/heading() همیشه جواب درست بدهند.
6
+ - هر فرمان دیداری وارد یک صف واحد FIFO می‌شود (چه در اسکریپت، چه در callback).
7
+ - tick() هر فریم صف را خالی می‌کند: با انیمیشن روی state «بصری»، یا آنی
8
+ (speed == 0 یا tracer(0)). چون callback ها هم به همین صف می‌ریزند، به حالت
9
+ LIVE جداگانه نیازی نیست — ترتیب همیشه حفظ است.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import atexit
15
+ import math
16
+ import sys
17
+ import weakref
18
+ from collections import deque
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+
22
+ from ._commands import InstantCmd, MoveCmd, RotateCmd
23
+ from ._events import EventRegistry
24
+ from ._renderer import Renderer
25
+ from ._state import TurtleState
26
+
27
+ # سرعت‌های پخش (جدول قطعی پلن)
28
+ MOVE_PX_PER_SEC = 250
29
+ ROTATE_DEG_PER_SEC = 400
30
+
31
+
32
+ class Mode(Enum):
33
+ RECORDING = "recording" # قبل از done()
34
+ PLAYING = "playing" # داخل حلقه arcade
35
+
36
+
37
+ @dataclass
38
+ class TurtleRuntime:
39
+ logical: TurtleState
40
+ visual: TurtleState
41
+
42
+
43
+ class Engine:
44
+ def __init__(self):
45
+ self.mode = Mode.RECORDING
46
+ # توجه: این «mode» حالت اجرای موتور است (RECORDING/PLAYING)، نه
47
+ # turtle-mode ی standard/logo/world که Screen.mode() کنترل می‌کند —
48
+ # آن یکی عمداً نام دیگری دارد تا تداخل نکنند.
49
+ self.mode_name = "standard"
50
+ self.queue: deque = deque()
51
+ self.turtles: dict[int, TurtleRuntime] = {}
52
+ # شیءهای Turtle کاربر — weak تا لاک‌پشت دورریخته نشتی حافظه نسازد.
53
+ # Screen.reset() برای ریست‌کردن همه لاک‌پشت‌ها به این نیاز دارد، چون
54
+ # Turtle.reset() به فیلدهای روی خود شیء (مثل _pen_color_raw) دست می‌زند.
55
+ self.turtle_objects: weakref.WeakSet = weakref.WeakSet()
56
+ self.active_cmd = None
57
+ self._progress = 0.0
58
+ self.tracer_n = 1
59
+ self._tracer_counter = 0
60
+ self.renderer = Renderer()
61
+ self.events = EventRegistry(self)
62
+ self.window = None
63
+ # تنظیمات پنجره قبل از ساخته‌شدنش
64
+ self.pending_width = 800
65
+ self.pending_height = 600
66
+ self.pending_title = "ArcadeTurtle"
67
+ self.pending_bg = (255, 255, 255)
68
+ self._next_turtle_id = 0
69
+ self._ran = False
70
+
71
+ # ---------- لاک‌پشت‌ها ----------
72
+
73
+ def new_turtle(self) -> int:
74
+ tid = self._next_turtle_id
75
+ self._next_turtle_id += 1
76
+ st = TurtleState()
77
+ self.turtles[tid] = TurtleRuntime(logical=st, visual=st.copy_pose())
78
+ return tid
79
+
80
+ def visible_states(self):
81
+ return [rt.visual for rt in self.turtles.values()]
82
+
83
+ # ---------- پنجره ----------
84
+
85
+ def ensure_window(self):
86
+ if self.window is None:
87
+ from ._window import ArcadeTurtleWindow
88
+ self.window = ArcadeTurtleWindow(
89
+ self, self.pending_width, self.pending_height, self.pending_title
90
+ )
91
+ self.window.background_color = self.pending_bg
92
+ self.renderer.set_size(self.pending_width, self.pending_height)
93
+ return self.window
94
+
95
+ def set_bgcolor(self, color) -> None:
96
+ self.pending_bg = color
97
+ if self.window is not None:
98
+ self.window.background_color = color
99
+
100
+ def set_title(self, text: str) -> None:
101
+ self.pending_title = text
102
+ if self.window is not None:
103
+ self.window.set_caption(text)
104
+
105
+ def run(self) -> None:
106
+ """done()/mainloop() — پنجره را می‌سازد و حلقه arcade را شروع می‌کند."""
107
+ if self._ran:
108
+ return
109
+ self._ran = True
110
+ import arcade
111
+ self.ensure_window()
112
+ self.mode = Mode.PLAYING
113
+ arcade.run()
114
+
115
+ # ---------- صف و پخش ----------
116
+
117
+ def submit(self, cmd) -> None:
118
+ self.queue.append(cmd)
119
+
120
+ def set_tracer(self, n: int) -> None:
121
+ self.tracer_n = n
122
+ if n == 0 and self.active_cmd is not None:
123
+ # فرمان نیمه‌کاره فوراً تمام شود
124
+ self.renderer.clear_live_line()
125
+ self._apply_instant(self.active_cmd)
126
+ self.active_cmd = None
127
+
128
+ def flush_turtle(self, turtle_id: int) -> None:
129
+ """فرمان‌های در صف یا در حال انیمیشنِ این لاک‌پشت را آنی اعمال می‌کند
130
+ (بدون دست‌زدن به فرمان‌های بقیه لاک‌پشت‌ها). undo() قبل از برگرداندنِ
131
+ آخرین اقدام از این استفاده می‌کند تا مطمئن شود چیزی نیمه‌کاره نمانده —
132
+ وگرنه معلوم نیست چه چیزی را باید undo کرد: خطِ هنوز پخته‌نشده، یا
133
+ فرمانی که هنوز حتی از صف بیرون نیامده."""
134
+ if self.active_cmd is not None and self.active_cmd.turtle_id == turtle_id:
135
+ self.renderer.clear_live_line()
136
+ self._apply_instant(self.active_cmd)
137
+ self.active_cmd = None
138
+ remaining = deque()
139
+ for cmd in self.queue:
140
+ if cmd.turtle_id != turtle_id:
141
+ remaining.append(cmd)
142
+ elif isinstance(cmd, InstantCmd):
143
+ cmd.fn()
144
+ else:
145
+ self._apply_instant(cmd)
146
+ self.queue = remaining
147
+
148
+ def tick(self, dt: float) -> None:
149
+ remaining = dt
150
+ while True:
151
+ if self.active_cmd is None:
152
+ if not self.queue:
153
+ return
154
+ cmd = self.queue.popleft()
155
+ if isinstance(cmd, InstantCmd):
156
+ cmd.fn()
157
+ continue
158
+ if self.tracer_n == 0 or cmd.speed == 0:
159
+ self._apply_instant(cmd)
160
+ continue
161
+ if self.tracer_n > 1:
162
+ # مثل tracer(n) در turtle اصلی: فقط هر n اُمین حرکت
163
+ # واقعاً پخش/انیمیشن می‌شود؛ بقیه آنی اعمال می‌شوند تا
164
+ # صحنه سریع‌تر جلو برود (ولی موقعیت منطقی هرگز نمی‌پرد،
165
+ # چون آن از قبل هم‌زمان با فراخوانی API به‌روز شده بود).
166
+ self._tracer_counter += 1
167
+ if self._tracer_counter % self.tracer_n != 0:
168
+ self._apply_instant(cmd)
169
+ continue
170
+ self.active_cmd = cmd
171
+ self._progress = 0.0
172
+ if remaining <= 0:
173
+ return
174
+ remaining = self._advance(remaining)
175
+ if self.active_cmd is not None:
176
+ return # فرمان فعال هنوز تمام نشده؛ فریم بعد ادامه می‌یابد
177
+
178
+ def _visual(self, turtle_id: int) -> TurtleState:
179
+ return self.turtles[turtle_id].visual
180
+
181
+ def _apply_instant(self, cmd) -> None:
182
+ v = self._visual(cmd.turtle_id)
183
+ if isinstance(cmd, MoveCmd):
184
+ if cmd.pen_down:
185
+ self.renderer.add_line(cmd.turtle_id, cmd.start, cmd.end, cmd.color, cmd.size)
186
+ v.x, v.y = cmd.end
187
+ elif isinstance(cmd, RotateCmd):
188
+ v.heading = cmd.end_heading % 360.0
189
+
190
+ def _advance(self, dt: float) -> float:
191
+ """فرمان فعال را به‌اندازه dt جلو می‌برد؛ زمانِ مصرف‌نشده را برمی‌گرداند."""
192
+ cmd = self.active_cmd
193
+ v = self._visual(cmd.turtle_id)
194
+ if isinstance(cmd, MoveCmd):
195
+ length = math.dist(cmd.start, cmd.end)
196
+ pps = cmd.speed * MOVE_PX_PER_SEC
197
+ needed = (length - self._progress) / pps if pps > 0 else 0.0
198
+ if dt >= needed or length == 0:
199
+ self.renderer.clear_live_line()
200
+ self._apply_instant(cmd)
201
+ self.active_cmd = None
202
+ return dt - needed
203
+ self._progress += dt * pps
204
+ t = self._progress / length
205
+ cur = (
206
+ cmd.start[0] + (cmd.end[0] - cmd.start[0]) * t,
207
+ cmd.start[1] + (cmd.end[1] - cmd.start[1]) * t,
208
+ )
209
+ v.x, v.y = cur
210
+ if cmd.pen_down:
211
+ self.renderer.set_live_line(cmd.start, cur, cmd.color, cmd.size)
212
+ return 0.0
213
+ if isinstance(cmd, RotateCmd):
214
+ total = abs(cmd.delta)
215
+ dps = cmd.speed * ROTATE_DEG_PER_SEC
216
+ needed = (total - self._progress) / dps if dps > 0 else 0.0
217
+ if dt >= needed or total == 0:
218
+ self._apply_instant(cmd)
219
+ self.active_cmd = None
220
+ return dt - needed
221
+ self._progress += dt * dps
222
+ sign = 1.0 if cmd.delta >= 0 else -1.0
223
+ v.heading = (cmd.start_heading + sign * self._progress) % 360.0
224
+ return 0.0
225
+ # نوع ناشناخته — نباید رخ دهد
226
+ self.active_cmd = None
227
+ return dt
228
+
229
+
230
+ _engine: Engine | None = None
231
+ _atexit_registered = False
232
+ _died_with_exception = False
233
+
234
+
235
+ def _install_excepthook() -> None:
236
+ """اگر اسکریپت با یک خطای واقعی بمیرد علامت می‌زند.
237
+
238
+ بدون این، هنرجویی که برنامه‌اش با خطای خودش کرش کرده بعد از traceback
239
+ واقعی پیام «done() را فراموش کرده‌ای» را هم می‌بیند و دنبال مشکل اشتباه
240
+ می‌رود. به‌جای sys.last_value (که رفتارش بین ۳.۱۰ تا ۳.۱۳ فرق دارد)
241
+ این‌جا hook قبلی را زنجیر می‌کنیم تا با pytest/IDE هم تداخل نکند.
242
+ """
243
+ previous = sys.excepthook
244
+
245
+ def hook(exc_type, exc, tb):
246
+ global _died_with_exception
247
+ _died_with_exception = True
248
+ previous(exc_type, exc, tb)
249
+
250
+ sys.excepthook = hook
251
+
252
+
253
+ def _warn_if_done_forgotten() -> None:
254
+ eng = _engine
255
+ if eng is None or eng._ran or not eng.turtles or _died_with_exception:
256
+ return
257
+ print(
258
+ "\n⚠️ به نظر می‌رسد done() را فراموش کرده‌ای؛ "
259
+ "بدون آن پنجره هرگز باز نمی‌شود.\n"
260
+ " آخرین خط برنامه‌ات را با turtle.done() تمام کن.\n",
261
+ file=sys.stderr,
262
+ )
263
+
264
+
265
+ def get_engine() -> Engine:
266
+ global _engine, _atexit_registered
267
+ if _engine is None:
268
+ _engine = Engine()
269
+ if not _atexit_registered:
270
+ atexit.register(_warn_if_done_forgotten)
271
+ _install_excepthook()
272
+ _atexit_registered = True
273
+ return _engine
274
+
275
+
276
+ def reset_engine() -> None:
277
+ """فقط برای تست‌ها — موتور فعلی را «مصرف‌شده» علامت می‌زند تا هشدار
278
+ فراموشیِ done() برای اسکریپت‌های آزمایشی که عمداً done() صدا نمی‌زنند
279
+ نمایش داده نشود. حالت رنگ هم به پیش‌فرض برمی‌گردد تا تست‌ها به هم نشت نکنند."""
280
+ global _engine
281
+ if _engine is not None:
282
+ _engine._ran = True
283
+ _engine = None
284
+ from ._errors import set_colormode
285
+ set_colormode(255)
@@ -0,0 +1,128 @@
1
+ """خطاهای مهربان فارسی و جدول رنگ‌ها."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class ArcadeTurtleError(Exception):
7
+ """خطای آموزشی با پیام فارسی."""
8
+
9
+
10
+ class Terminator(Exception):
11
+ """فقط برای سازگاری import (`from turtle import Terminator`).
12
+
13
+ در turtle استاندارد وقتی کاربر پنجره را می‌بندد ولی اسکریپت هنوز در
14
+ حلقه‌ی خودش دستور می‌دهد، این استثنا برای متوقف‌کردن آن حلقه raise
15
+ می‌شود. در ArcadeTurtle نیازی به آن نیست: بستن پنجره فقط باعث پایان
16
+ done()/mainloop() می‌شود و اسکریپت به همان‌جا برمی‌گردد، نه ادامه‌ی
17
+ یک حلقه‌ی بی‌نهایت داخل موتور."""
18
+
19
+
20
+ # رنگ‌های رایج آموزشی (نام‌های Tk/CSS که در آموزش‌های turtle استفاده می‌شوند)
21
+ COLOR_NAMES: dict[str, tuple[int, int, int]] = {
22
+ "black": (0, 0, 0), "white": (255, 255, 255),
23
+ "red": (255, 0, 0), "green": (0, 128, 0), "blue": (0, 0, 255),
24
+ "yellow": (255, 255, 0), "orange": (255, 165, 0), "purple": (128, 0, 128),
25
+ "pink": (255, 192, 203), "brown": (165, 42, 42), "gray": (128, 128, 128),
26
+ "grey": (128, 128, 128), "lightgray": (211, 211, 211),
27
+ "lightgrey": (211, 211, 211), "darkgray": (169, 169, 169),
28
+ "darkgrey": (169, 169, 169), "cyan": (0, 255, 255), "magenta": (255, 0, 255),
29
+ "lime": (0, 255, 0), "limegreen": (50, 205, 50), "navy": (0, 0, 128),
30
+ "teal": (0, 128, 128), "olive": (128, 128, 0), "maroon": (128, 0, 0),
31
+ "silver": (192, 192, 192), "gold": (255, 215, 0), "violet": (238, 130, 238),
32
+ "indigo": (75, 0, 130), "turquoise": (64, 224, 208), "salmon": (250, 128, 114),
33
+ "coral": (255, 127, 80), "tomato": (255, 99, 71), "orchid": (218, 112, 214),
34
+ "plum": (221, 160, 221), "khaki": (240, 230, 140), "beige": (245, 245, 220),
35
+ "ivory": (255, 255, 240), "snow": (255, 250, 250),
36
+ "skyblue": (135, 206, 235), "lightblue": (173, 216, 230),
37
+ "royalblue": (65, 105, 225), "dodgerblue": (30, 144, 255),
38
+ "steelblue": (70, 130, 180), "midnightblue": (25, 25, 112),
39
+ "darkblue": (0, 0, 139), "darkgreen": (0, 100, 0),
40
+ "forestgreen": (34, 139, 34), "seagreen": (46, 139, 87),
41
+ "springgreen": (0, 255, 127), "greenyellow": (173, 255, 47),
42
+ "darkred": (139, 0, 0), "crimson": (220, 20, 60), "firebrick": (178, 34, 34),
43
+ "hotpink": (255, 105, 180), "deeppink": (255, 20, 147),
44
+ "lavender": (230, 230, 250), "wheat": (245, 222, 179), "tan": (210, 180, 140),
45
+ "chocolate": (210, 105, 30), "sienna": (160, 82, 45),
46
+ "slategray": (112, 128, 144), "slategrey": (112, 128, 144),
47
+ "aqua": (0, 255, 255), "fuchsia": (255, 0, 255),
48
+ "lightgreen": (144, 238, 144), "lightyellow": (255, 255, 224),
49
+ "lightpink": (255, 182, 193), "peachpuff": (255, 218, 185),
50
+ }
51
+
52
+
53
+ # حالت رنگ، دقیقاً مثل turtle استاندارد: پیش‌فرض ۲۵۵.
54
+ # قبلاً به‌جای این، حدس زده می‌شد که «اگر هر سه عدد بین ۰ و ۱ بودند یعنی اعشاری»،
55
+ # ولی آن حدس (1, 1, 1) را سفید می‌کرد در حالی که turtle اصلی آن را تقریباً سیاه
56
+ # می‌بیند — تله‌ای برای هنرجویی که آموزش ۰-۲۵۵ را دنبال می‌کند.
57
+ _colormode: float = 255
58
+
59
+
60
+ def get_colormode() -> float:
61
+ return _colormode
62
+
63
+
64
+ def set_colormode(cmode) -> None:
65
+ global _colormode
66
+ if cmode in (1, 1.0):
67
+ _colormode = 1.0
68
+ elif cmode == 255:
69
+ _colormode = 255
70
+ else:
71
+ raise ArcadeTurtleError(
72
+ f"colormode فقط 1.0 یا 255 می‌تواند باشد، نه {cmode!r}\n"
73
+ "colormode(255) یعنی رنگ‌ها 0 تا 255، colormode(1.0) یعنی 0 تا 1"
74
+ )
75
+
76
+
77
+ def resolve_color(value) -> tuple[int, int, int]:
78
+ """نام رنگ / '#rrggbb' / تاپل (r,g,b) را به تاپل ۰-۲۵۵ تبدیل می‌کند."""
79
+ if isinstance(value, str):
80
+ name = value.strip().lower().replace(" ", "")
81
+ if name in COLOR_NAMES:
82
+ return COLOR_NAMES[name]
83
+ if name.startswith("#") and len(name) == 7:
84
+ try:
85
+ return tuple(int(name[i:i + 2], 16) for i in (1, 3, 5))
86
+ except ValueError:
87
+ pass
88
+ raise ArcadeTurtleError(
89
+ f"رنگ '{value}' را نمی‌شناسم. 🎨\n"
90
+ "نمونه رنگ‌های درست: red, blue, green, yellow, purple, orange\n"
91
+ "یا کد رنگی مثل '#ff0000' یا تاپل مثل (255, 0, 0)"
92
+ )
93
+ if isinstance(value, (tuple, list)) and len(value) == 3:
94
+ try:
95
+ nums = [float(c) for c in value]
96
+ except (TypeError, ValueError):
97
+ raise ArcadeTurtleError(
98
+ f"رنگ {value!r} معتبر نیست؛ سه عدد لازم است، مثل (255, 0, 0)"
99
+ ) from None
100
+ top = _colormode
101
+ if all(0 <= c <= top for c in nums):
102
+ if top == 1.0:
103
+ return tuple(round(c * 255) for c in nums)
104
+ return tuple(round(c) for c in nums)
105
+ if top == 1.0:
106
+ raise ArcadeTurtleError(
107
+ f"اعداد رنگ {value!r} باید بین 0 تا 1 باشند، "
108
+ "چون colormode(1.0) فعال است.\n"
109
+ "برای رنگ‌های 0 تا 255 اول colormode(255) را صدا بزن."
110
+ )
111
+ raise ArcadeTurtleError(
112
+ f"اعداد رنگ {value!r} باید بین 0 تا 255 باشند.\n"
113
+ "برای رنگ‌های اعشاری (0 تا 1) اول colormode(1.0) را صدا بزن."
114
+ )
115
+ raise ArcadeTurtleError(
116
+ f"رنگ {value!r} معتبر نیست.\n"
117
+ "رنگ می‌تواند نام انگلیسی ('red')، کد ('#ff0000') یا تاپل (255, 0, 0) باشد."
118
+ )
119
+
120
+
121
+ def require_number(value, func_name: str, example: str = "100"):
122
+ """اگر value عدد نبود، خطای فارسی آموزشی می‌دهد."""
123
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
124
+ raise ArcadeTurtleError(
125
+ f"{func_name} یک عدد می‌خواهد، مثل {func_name}({example})\n"
126
+ f"ولی به آن {value!r} داده شد."
127
+ )
128
+ return value