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.
- arcadeturtle/__init__.py +33 -0
- arcadeturtle/_commands.py +45 -0
- arcadeturtle/_coords.py +46 -0
- arcadeturtle/_engine.py +285 -0
- arcadeturtle/_errors.py +128 -0
- arcadeturtle/_events.py +207 -0
- arcadeturtle/_renderer.py +284 -0
- arcadeturtle/_shapes.py +213 -0
- arcadeturtle/_state.py +68 -0
- arcadeturtle/_text_fa.py +24 -0
- arcadeturtle/_vec2d.py +49 -0
- arcadeturtle/_window.py +48 -0
- arcadeturtle/api.py +257 -0
- arcadeturtle/assets/fonts/Vazirmatn-Regular.ttf +0 -0
- arcadeturtle/screen.py +264 -0
- arcadeturtle/turtle_obj.py +886 -0
- arcadeturtle-0.1.0.dist-info/METADATA +192 -0
- arcadeturtle-0.1.0.dist-info/RECORD +20 -0
- arcadeturtle-0.1.0.dist-info/WHEEL +4 -0
- arcadeturtle-0.1.0.dist-info/licenses/LICENSE +21 -0
arcadeturtle/__init__.py
ADDED
|
@@ -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]
|
arcadeturtle/_coords.py
ADDED
|
@@ -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
|
arcadeturtle/_engine.py
ADDED
|
@@ -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)
|
arcadeturtle/_errors.py
ADDED
|
@@ -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
|