pycoode 1.0.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.
pycoode/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """pycoode —— 让做游戏像写一行代码一样简单
2
+
3
+ 基于 pygame 封装: 自动处理窗口创建、中文字体、高 DPI、
4
+ 分辨率适配、游戏循环、输入、精灵、音效和场景。
5
+ """
6
+
7
+ from .core import Game, run, keycode
8
+ from .sprites import Sprite, load_image
9
+ from .scene import Scene
10
+ from .audio import (
11
+ play, music, stop_music, pause_music, resume_music,
12
+ set_music_volume, is_music_playing,
13
+ )
14
+ from .utils import (
15
+ Vector, to_color, clamp, lerp, dist, angle,
16
+ rand, randint, choice, chance,
17
+ )
18
+ from .demo import demo
19
+
20
+ __version__ = "1.0.0"
21
+
22
+
23
+ def sprite(path, x=0, y=0, **kw):
24
+ """加载精灵: pc.sprite("hero.png", 400, 300)"""
25
+ return Sprite.load(path, x, y, **kw)
26
+
27
+
28
+ __all__ = [
29
+ "Game", "run", "demo", "keycode",
30
+ "Sprite", "sprite", "load_image",
31
+ "Scene",
32
+ "play", "music", "stop_music", "pause_music", "resume_music",
33
+ "set_music_volume", "is_music_playing",
34
+ "Vector", "to_color", "clamp", "lerp", "dist", "angle",
35
+ "rand", "randint", "choice", "chance",
36
+ "__version__",
37
+ ]
pycoode/audio.py ADDED
@@ -0,0 +1,69 @@
1
+ """pycoode.audio —— 音效与背景音乐,一行播放
2
+
3
+ 没有音频设备时所有函数静默降级,不会让游戏崩掉。
4
+ """
5
+
6
+ import pygame
7
+
8
+ _sound_cache = {}
9
+ _mixer_ok = True
10
+
11
+
12
+ def _ensure_mixer():
13
+ global _mixer_ok
14
+ if not _mixer_ok:
15
+ return False
16
+ try:
17
+ if not pygame.mixer.get_init():
18
+ pygame.mixer.init()
19
+ return True
20
+ except Exception:
21
+ _mixer_ok = False
22
+ return False
23
+
24
+
25
+ def play(path, volume=1.0):
26
+ """播放音效(可重复调用,自动缓存)"""
27
+ if not _ensure_mixer():
28
+ return None
29
+ key = str(path)
30
+ if key not in _sound_cache:
31
+ _sound_cache[key] = pygame.mixer.Sound(key)
32
+ s = _sound_cache[key]
33
+ s.set_volume(max(0.0, min(1.0, volume)))
34
+ s.play()
35
+ return s
36
+
37
+
38
+ def music(path, loop=True, volume=0.6, fade_ms=0):
39
+ """播放背景音乐(默认循环)"""
40
+ if not _ensure_mixer():
41
+ return
42
+ pygame.mixer.music.load(str(path))
43
+ pygame.mixer.music.set_volume(max(0.0, min(1.0, volume)))
44
+ pygame.mixer.music.play(-1 if loop else 0, fade_ms=fade_ms)
45
+
46
+
47
+ def stop_music(fade_ms=0):
48
+ if not _ensure_mixer():
49
+ return
50
+ pygame.mixer.music.fadeout(fade_ms) if fade_ms else pygame.mixer.music.stop()
51
+
52
+
53
+ def pause_music():
54
+ if _ensure_mixer():
55
+ pygame.mixer.music.pause()
56
+
57
+
58
+ def resume_music():
59
+ if _ensure_mixer():
60
+ pygame.mixer.music.unpause()
61
+
62
+
63
+ def set_music_volume(v):
64
+ if _ensure_mixer():
65
+ pygame.mixer.music.set_volume(max(0.0, min(1.0, v)))
66
+
67
+
68
+ def is_music_playing():
69
+ return _ensure_mixer() and pygame.mixer.music.get_busy()
pycoode/core.py ADDED
@@ -0,0 +1,455 @@
1
+ """pycoode.core —— Game 类与一行入口 run()
2
+
3
+ 解决"适配问题"的三个设计:
4
+ 1. 虚拟画布 + 信箱缩放: 游戏永远按设计分辨率(如 800x600)写坐标,
5
+ 窗口任意拉伸都不变形、不错位;
6
+ 2. Windows 高 DPI 适配, 笔记本缩放 150% 也不会模糊偏移;
7
+ 3. 中文自动匹配字体, text("你好") 不开字体文件也能显示。
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ import pygame
14
+
15
+ from .gfx import DrawingMixin
16
+ from .utils import to_color
17
+
18
+ # Windows 高 DPI: 必须在 pygame.init() 之前设置
19
+ if sys.platform.startswith("win"):
20
+ os.environ.setdefault("SDL_WINDOWS_DPI_AWARENESS", "per monitor")
21
+
22
+ # 常用按键名 -> pygame 键值, 让 g.key_held("space") 这种写法可用
23
+ KEY_NAMES = {
24
+ "space": pygame.K_SPACE, "enter": pygame.K_RETURN, "return": pygame.K_RETURN,
25
+ "esc": pygame.K_ESCAPE, "escape": pygame.K_ESCAPE, "tab": pygame.K_TAB,
26
+ "up": pygame.K_UP, "down": pygame.K_DOWN, "left": pygame.K_LEFT, "right": pygame.K_RIGHT,
27
+ "shift": pygame.K_LSHIFT, "ctrl": pygame.K_LCTRL, "control": pygame.K_LCTRL,
28
+ "alt": pygame.K_LALT, "backspace": pygame.K_BACKSPACE, "delete": pygame.K_DELETE,
29
+ "home": pygame.K_HOME, "end": pygame.K_END, "pageup": pygame.K_PAGEUP, "pagedown": pygame.K_PAGEDOWN,
30
+ "insert": pygame.K_INSERT, "capslock": pygame.K_CAPSLOCK,
31
+ **{chr(c): c for c in range(ord("a"), ord("z") + 1)},
32
+ **{d: getattr(pygame, f"K_{d}") for d in "0123456789"},
33
+ **{f"f{i}": getattr(pygame, f"K_F{i}") for i in range(1, 13)},
34
+ }
35
+
36
+
37
+ def keycode(key):
38
+ """把 'space'/'a'/pygame.K_SPACE 统一成 pygame 键值; 无法识别返回 None"""
39
+ if isinstance(key, int):
40
+ return key
41
+ return KEY_NAMES.get(str(key).strip().lower())
42
+
43
+
44
+ # 常用窗口尺寸预设: pc.run(size="hd")
45
+ SIZE_PRESETS = {
46
+ "small": (640, 480),
47
+ "md": (800, 600),
48
+ "large": (1024, 768),
49
+ "square": (600, 600),
50
+ "hd": (1280, 720),
51
+ "fullhd": (1920, 1080),
52
+ }
53
+
54
+
55
+ def _parse_size(size, fallback):
56
+ """把 'hd' / (1280, 720) 统一成 (宽, 高)"""
57
+ if size is None:
58
+ return fallback
59
+ if isinstance(size, str):
60
+ key = size.strip().lower()
61
+ if key in SIZE_PRESETS:
62
+ return SIZE_PRESETS[key]
63
+ raise ValueError(
64
+ f"未知尺寸预设 {size!r}, 可用: {', '.join(sorted(SIZE_PRESETS))}")
65
+ if isinstance(size, (tuple, list)) and len(size) == 2:
66
+ return (int(size[0]), int(size[1]))
67
+ raise TypeError("size 需要是 (宽, 高) 元组或预设名字, 如 size='hd'")
68
+
69
+
70
+ def _desktop_size():
71
+ """当前桌面分辨率; 取不到返回 None(如 dummy 驱动 / 无头环境)"""
72
+ try:
73
+ modes = pygame.display.list_modes()
74
+ if modes and modes != -1 and modes[0][0] > 0:
75
+ return modes[0]
76
+ except Exception:
77
+ pass
78
+ return None
79
+
80
+
81
+ class Game(DrawingMixin):
82
+ """pycoode 游戏主类。
83
+
84
+ 最简用法:
85
+ import pycoode as pc
86
+ pc.run() # 直接弹出窗口
87
+ pc.run(draw=lambda g: g.text("Hi", 10, 10))
88
+
89
+ 完整用法:
90
+ g = pc.Game(800, 600, title="我的游戏", fps=60)
91
+ g.on_update = lambda g, dt: ...
92
+ g.on_draw = lambda g: ...
93
+ g.run()
94
+ """
95
+
96
+ def __init__(self, width=800, height=600, title="pycoode", fps=60,
97
+ background="dark", resizable=True, fullscreen=False,
98
+ show_fps=False, icon=None, quit_key="escape",
99
+ size=None, fit_screen=False, mouse_visible=True):
100
+ pygame.init()
101
+ try:
102
+ pygame.font.init()
103
+ except Exception:
104
+ pass
105
+ try:
106
+ pygame.mixer.init()
107
+ except Exception:
108
+ pass
109
+
110
+ # 窗口大小: size 预设/元组优先, 其次 fit_screen 自动收缩到屏幕内
111
+ if size is not None:
112
+ width, height = _parse_size(size, (width, height))
113
+ if fit_screen:
114
+ desktop = _desktop_size()
115
+ if desktop:
116
+ width = min(width, max(320, int(desktop[0] * 0.9)))
117
+ height = min(height, max(240, int(desktop[1] * 0.9)))
118
+
119
+ self.resizable = bool(resizable)
120
+ self._fullscreen = bool(fullscreen)
121
+ flags = 0
122
+ if self.resizable:
123
+ flags |= pygame.RESIZABLE
124
+ if self._fullscreen:
125
+ flags |= pygame.FULLSCREEN
126
+ self.screen = pygame.display.set_mode((int(width), int(height)), flags)
127
+ pygame.display.set_caption(str(title))
128
+ if icon:
129
+ try:
130
+ pygame.display.set_icon(pygame.image.load(str(icon)))
131
+ except Exception:
132
+ pass
133
+
134
+ # 设计分辨率(虚拟画布) vs 真实窗口, 两者解耦是"零适配"的关键
135
+ self.width = int(width)
136
+ self.height = int(height)
137
+ self.canvas = pygame.Surface((self.width, self.height))
138
+
139
+ self.title = str(title)
140
+ self.fps = int(fps)
141
+ self.background = background
142
+ self.quit_key = keycode(quit_key) or pygame.K_ESCAPE
143
+ self.show_fps = show_fps
144
+
145
+ # 鼠标指针可见性(游戏里常隐藏)
146
+ self.mouse_visible = bool(mouse_visible)
147
+ try:
148
+ pygame.mouse.set_visible(self.mouse_visible)
149
+ except Exception:
150
+ pass
151
+
152
+ self.running = False
153
+ self.clock = pygame.time.Clock()
154
+ self.dt = 1.0 / self.fps
155
+ self.time = 0.0
156
+ self.frame = 0
157
+
158
+ # ---- 输入状态 ----
159
+ self.keys = set() # 当前按住
160
+ self._pressed = set() # 本帧刚按下
161
+ self._released = set() # 本帧刚松开
162
+ self.mouse_x = 0.0
163
+ self.mouse_y = 0.0
164
+ self.mouse_buttons = set()
165
+ self._mouse_pressed = set()
166
+ self._mouse_released = set()
167
+ self.touches = [] # [(x, y, finger_id), ...] 触摸点
168
+
169
+ # ---- 回调 / 场景 ----
170
+ self.on_update = None
171
+ self.on_draw = None
172
+ self.on_key = None # on_key(key)
173
+ self.on_click = None # on_click(x, y, button)
174
+ self.scene = None
175
+ self._scene_stack = []
176
+ self._scene_ops = [] # 本帧待执行的场景切换, 帧初统一应用
177
+
178
+ # ========================================================================
179
+ # 主循环
180
+ # ========================================================================
181
+
182
+ def run(self):
183
+ """启动游戏主循环(阻塞,直到退出)"""
184
+ self.running = True
185
+ try:
186
+ while self.running:
187
+ dt = self.clock.tick(self.fps) / 1000.0
188
+ self._frame(min(dt, 0.1)) # 掉帧时 dt 上限 100ms, 防穿墙
189
+ finally:
190
+ self._shutdown()
191
+
192
+ def step(self, dt=None):
193
+ """手动推进一步(测试 / 自己写循环时用)"""
194
+ if dt is None:
195
+ dt = min(self.clock.tick(self.fps) / 1000.0, 0.1)
196
+ self._frame(dt)
197
+
198
+ def _frame(self, dt):
199
+ self.dt = dt
200
+ self.time += dt
201
+ self.frame += 1
202
+ self._apply_scene_ops()
203
+ self._poll_events()
204
+ self._update(dt)
205
+ self._draw()
206
+
207
+ def _shutdown(self):
208
+ try:
209
+ pygame.quit()
210
+ except Exception:
211
+ pass
212
+
213
+ def stop(self):
214
+ """退出游戏"""
215
+ self.running = False
216
+
217
+ def _update(self, dt):
218
+ if self.scene is not None:
219
+ self.scene.update(self, dt)
220
+ if self.on_update:
221
+ self.on_update(self, dt)
222
+
223
+ def _draw(self):
224
+ self.canvas.fill(to_color(self.background))
225
+ if self.scene is not None:
226
+ self.scene.draw(self)
227
+ if self.on_draw:
228
+ self.on_draw(self)
229
+ if self.show_fps:
230
+ self.text(f"FPS {self.clock.get_fps():.0f}", 8, 8, size=16,
231
+ color=(120, 255, 120))
232
+ self._present()
233
+
234
+ # ========================================================================
235
+ # 缩放适配: 虚拟画布 -> 真实窗口 (信箱式, 保持比例)
236
+ # ========================================================================
237
+
238
+ def _view(self):
239
+ """返回 (scale, offset_x, offset_y)"""
240
+ sw, sh = self.screen.get_size()
241
+ if sw == 0 or sh == 0:
242
+ return 1.0, 0, 0
243
+ scale = min(sw / self.width, sh / self.height)
244
+ w, h = self.width * scale, self.height * scale
245
+ return scale, (sw - w) / 2.0, (sh - h) / 2.0
246
+
247
+ def _present(self):
248
+ scale, ox, oy = self._view()
249
+ w = max(1, int(self.width * scale))
250
+ h = max(1, int(self.height * scale))
251
+ self.screen.fill((0, 0, 0))
252
+ self.screen.blit(pygame.transform.smoothscale(self.canvas, (w, h)),
253
+ (int(ox), int(oy)))
254
+ pygame.display.flip()
255
+
256
+ def _to_canvas(self, sx, sy):
257
+ """屏幕坐标 -> 游戏内坐标"""
258
+ scale, ox, oy = self._view()
259
+ return (sx - ox) / scale, (sy - oy) / scale
260
+
261
+ # ========================================================================
262
+ # 窗口控制: 改窗口大小 / 全屏 / 鼠标指针
263
+ # ========================================================================
264
+
265
+ @property
266
+ def screen_size(self):
267
+ """真实窗口的像素大小(区别于游戏设计分辨率 g.width/g.height)"""
268
+ return self.screen.get_size()
269
+
270
+ def _recreate_window(self, w, h, fullscreen):
271
+ flags = 0
272
+ if fullscreen:
273
+ flags |= pygame.FULLSCREEN
274
+ elif self.resizable:
275
+ flags |= pygame.RESIZABLE
276
+ self.screen = pygame.display.set_mode((int(w), int(h)), flags)
277
+ self._fullscreen = bool(fullscreen)
278
+
279
+ def set_window_size(self, w, h):
280
+ """运行时改变窗口大小;游戏设计分辨率不变,画面自动缩放适配"""
281
+ self._recreate_window(w, h, False)
282
+
283
+ def toggle_fullscreen(self):
284
+ """全屏 / 窗口模式切换(快捷键 F11)"""
285
+ self._recreate_window(self.width, self.height, not self._fullscreen)
286
+
287
+ def set_mouse_visible(self, visible):
288
+ """显示 / 隐藏鼠标指针(游戏里常用)"""
289
+ self.mouse_visible = bool(visible)
290
+ try:
291
+ pygame.mouse.set_visible(self.mouse_visible)
292
+ except Exception:
293
+ pass
294
+
295
+ # ========================================================================
296
+ # 输入查询
297
+ # ========================================================================
298
+
299
+ @property
300
+ def mouse_pos(self):
301
+ return (self.mouse_x, self.mouse_y)
302
+
303
+ def key_held(self, key):
304
+ """按键当前是否按住"""
305
+ k = keycode(key)
306
+ return k is not None and k in self.keys
307
+
308
+ def key_pressed(self, key):
309
+ """按键本帧是否刚按下(按下瞬间只触发一帧)"""
310
+ k = keycode(key)
311
+ return k is not None and k in self._pressed
312
+
313
+ def key_released(self, key):
314
+ """按键本帧是否刚松开"""
315
+ k = keycode(key)
316
+ return k is not None and k in self._released
317
+
318
+ def mouse_held(self, button=1):
319
+ return button in self.mouse_buttons
320
+
321
+ def mouse_pressed(self, button=1):
322
+ return button in self._mouse_pressed
323
+
324
+ def mouse_released(self, button=1):
325
+ return button in self._mouse_released
326
+
327
+ # ========================================================================
328
+ # 场景管理
329
+ # ========================================================================
330
+
331
+ def switch_scene(self, scene):
332
+ self._scene_ops.append(("switch", scene))
333
+
334
+ def push_scene(self, scene):
335
+ self._scene_ops.append(("push", scene))
336
+
337
+ def pop_scene(self):
338
+ self._scene_ops.append(("pop", None))
339
+
340
+ def _apply_scene_ops(self):
341
+ while self._scene_ops:
342
+ op, arg = self._scene_ops.pop(0)
343
+ if op == "switch":
344
+ self._exit_scene(self.scene)
345
+ self._enter_scene(arg)
346
+ elif op == "push":
347
+ if self.scene is not None:
348
+ self._scene_stack.append(self.scene)
349
+ self._enter_scene(arg)
350
+ elif op == "pop":
351
+ self._exit_scene(self.scene)
352
+ self._enter_scene(self._scene_stack.pop() if self._scene_stack else None)
353
+
354
+ def _enter_scene(self, scene):
355
+ self.scene = scene
356
+ if scene is not None:
357
+ scene.game = self
358
+ scene.on_enter(self)
359
+
360
+ def _exit_scene(self, scene):
361
+ if scene is not None:
362
+ scene.on_exit(self)
363
+
364
+ # ========================================================================
365
+ # 事件
366
+ # ========================================================================
367
+
368
+ def _poll_events(self):
369
+ self._pressed.clear()
370
+ self._released.clear()
371
+ self._mouse_pressed.clear()
372
+ self._mouse_released.clear()
373
+
374
+ for e in pygame.event.get():
375
+ if e.type == pygame.QUIT:
376
+ self.stop()
377
+ elif e.type == pygame.KEYDOWN:
378
+ if e.key == self.quit_key:
379
+ self.stop()
380
+ continue
381
+ self.keys.add(e.key)
382
+ self._pressed.add(e.key)
383
+ if e.key == pygame.K_F11:
384
+ self.toggle_fullscreen()
385
+ if self.scene is not None:
386
+ self.scene.handle_key(self, e.key)
387
+ if self.on_key:
388
+ self.on_key(e.key)
389
+ elif e.type == pygame.KEYUP:
390
+ self.keys.discard(e.key)
391
+ self._released.add(e.key)
392
+ elif e.type == pygame.MOUSEMOTION:
393
+ self.mouse_x, self.mouse_y = self._to_canvas(*e.pos)
394
+ elif e.type == pygame.MOUSEBUTTONDOWN:
395
+ x, y = self._to_canvas(*e.pos)
396
+ self.mouse_x, self.mouse_y = x, y
397
+ self.mouse_buttons.add(e.button)
398
+ self._mouse_pressed.add(e.button)
399
+ if self.scene is not None:
400
+ self.scene.handle_click(self, x, y, e.button)
401
+ if self.on_click:
402
+ self.on_click(x, y, e.button)
403
+ elif e.type == pygame.MOUSEBUTTONUP:
404
+ self.mouse_buttons.discard(e.button)
405
+ self._mouse_released.add(e.button)
406
+ elif e.type == pygame.FINGERDOWN:
407
+ self._touch_add(e)
408
+ elif e.type == pygame.FINGERMOTION:
409
+ self._touch_add(e)
410
+ elif e.type == pygame.FINGERUP:
411
+ self.touches = [t for t in self.touches if t[2] != e.finger_id]
412
+ elif e.type == pygame.VIDEORESIZE:
413
+ self._recreate_window(e.w, e.h, self._fullscreen)
414
+
415
+ def _touch_add(self, e):
416
+ sw, sh = self.screen.get_size()
417
+ x, y = self._to_canvas(e.x * sw, e.y * sh)
418
+ self.touches = [t for t in self.touches if t[2] != e.finger_id]
419
+ self.touches.append((x, y, e.finger_id))
420
+
421
+
422
+ def run(update=None, draw=None, on_key=None, on_click=None, scene=None,
423
+ width=800, height=600, title="pycoode", fps=60, background="dark",
424
+ fullscreen=False, resizable=True, show_fps=False, icon=None,
425
+ quit_key="escape", size=None, fit_screen=False, mouse_visible=True):
426
+ """一行启动游戏。
427
+
428
+ pc.run() # 空窗口
429
+ pc.run(draw=lambda g: g.text("Hi", 10, 10)) # 画点东西
430
+ pc.run(update=my_update, draw=my_draw, width=960, height=640)
431
+ pc.run(scene=Menu()) # 用场景管理
432
+ pc.run(size="hd") # 预设窗口大小
433
+ pc.run(size=(1280, 720)) # 或直接给 (宽, 高)
434
+ pc.run(fit_screen=True) # 自动适应你的屏幕
435
+ pc.run(mouse_visible=False) # 隐藏鼠标指针
436
+ """
437
+ g = Game(width=width, height=height, title=title, fps=fps,
438
+ background=background, fullscreen=fullscreen,
439
+ resizable=resizable, show_fps=show_fps, icon=icon,
440
+ quit_key=quit_key, size=size, fit_screen=fit_screen,
441
+ mouse_visible=mouse_visible)
442
+ if scene is not None:
443
+ g.scene = scene() if isinstance(scene, type) else scene
444
+ g.scene.game = g
445
+ g.scene.on_enter(g)
446
+ if update is not None:
447
+ g.on_update = update
448
+ if draw is not None:
449
+ g.on_draw = draw
450
+ if on_key is not None:
451
+ g.on_key = on_key
452
+ if on_click is not None:
453
+ g.on_click = on_click
454
+ g.run()
455
+ return g
pycoode/demo.py ADDED
@@ -0,0 +1,59 @@
1
+ """pycoode.demo —— 内置演示小游戏
2
+
3
+ import pycoode
4
+ pycoode.demo()
5
+
6
+ 方向键 / WASD 移动青色方块,吃掉金色金币,ESC 退出。
7
+ """
8
+
9
+ from . import utils
10
+
11
+
12
+ def demo(width=800, height=600):
13
+ import pycoode as pc
14
+
15
+ player = pc.Sprite.solid("cyan", 36, 36, width / 2, height / 2)
16
+ coins = []
17
+ score = 0
18
+
19
+ def spawn_coin():
20
+ coins.append(pc.Sprite.solid(
21
+ "gold", 18, 18,
22
+ utils.rand(40, width - 40), utils.rand(40, height - 40),
23
+ ))
24
+
25
+ for _ in range(5):
26
+ spawn_coin()
27
+
28
+ def update(g, dt):
29
+ nonlocal score
30
+ speed = 320
31
+ dx = int(g.key_held("right")) - int(g.key_held("left"))
32
+ dy = int(g.key_held("down")) - int(g.key_held("up"))
33
+ if g.key_held("d"):
34
+ dx += 1
35
+ if g.key_held("a"):
36
+ dx -= 1
37
+ if g.key_held("s"):
38
+ dy += 1
39
+ if g.key_held("w"):
40
+ dy -= 1
41
+ player.x = utils.clamp(player.x + dx * speed * dt, 18, width - 18)
42
+ player.y = utils.clamp(player.y + dy * speed * dt, 18, height - 18)
43
+ for c in list(coins):
44
+ if player.collides_with(c):
45
+ coins.remove(c)
46
+ score += 1
47
+ spawn_coin()
48
+
49
+ def draw(g):
50
+ g.clear("dark")
51
+ for c in coins:
52
+ c.draw(g)
53
+ player.draw(g)
54
+ g.text(f"得分 {score}", 16, 14, size=26, color="white")
55
+ g.text("方向键 / WASD 移动 · ESC 退出", 16, height - 34,
56
+ size=16, color=(150, 150, 160))
57
+
58
+ pc.run(update=update, draw=draw, width=width, height=height,
59
+ title="pycoode demo")