python/app/breakout.py · 31673 字节 · 下载原文
# breakout.py —— 打砖块(VML 的 Python 前端)
# breakout.py — Breakout (VML's Python frontend)
#
# 界面走**共享调用库** `Lib/shared/src/vmlui.c`(编成 `vmlui.vml`,由 `vmltool.config.xml`
# The UI goes through the **shared call library** `Lib/shared/src/vmlui.c` (compiled into `vmlui.vml`,
# 挂到 python 的 `Libs` 上):Python 前端不支持内联 `asm()`,只能按标签调 C 函数 ——
# attached to python's `Libs` by `vmltool.config.xml`): the Python frontend has no inline asm(), so
# 这正是那座共享库存在的意义。
# it can only call C functions by label — which is exactly why that shared library exists.
# ↳ 2026-10-04 复验仍成立:`asm("SYSCALL 6")` 实测 `error: 未定义的函数 'asm'(引用 2 次)`(`asm` 被当普通调用)。
# ↳ Still true, re-checked 2026-10-04: `asm("SYSCALL 6")` measures `error: undefined function 'asm' (referenced 2 times)` (`asm` is treated as an ordinary call).
#
# ## ⚠ 本程序是**平铺**的(一个 `def` 都没有)—— 当时是被前端逼的,**那几条现在都修好了**
# ## ⚠ This program is **flat** (not a single `def`) — the frontend forced it at the time;
# ## **those limits have since been fixed**, see below.
#
# 写这个游戏时实测出来的三条前端限制,当时任何一条都足以让"有函数的结构"跑不通;
# Three frontend limits measured while writing this game, each of which broke a "structured" program.
# **这轮已经把三条全修了**(细节与判据见 `FRONTEND_DEFECTS.md` 的 Python 节),
# **All three are fixed in the same round** (details and criteria in the Python section of
# 同目录的 `wordle.py` 就是**修好之后**用正常结构写的(模块级状态 + 只读小函数)。
# `FRONTEND_DEFECTS.md`); `wordle.py` beside this file is written **after** the fix in the normal
# 本文件保持平铺**只是因为它已经写好、跑通了**,不是"现在还得这么写"。
# style (module-level state + small read-only functions). This file stays flat simply because it is
# already written and working — not because it still has to be.
#
# ① **函数里看不见模块级变量**(当时)⇒ 现在**读得到了**(标量与列表都一样),
# ① **Module-level variables were invisible inside a function** ⇒ now they are **readable**
# 顶层 `x = …` 会登记成一格数据段 `module_x`,函数体里读到的就是同一份。
# (scalars and lists alike): a top-level `x = …` is registered as a data-section slot
# `module_x`, and a function body reads that very slot.
#
# ② **多参函数收不到第 2 个及以后的实参**(当时:调用方按栈传参、被调用方序言却从
# ② **Multi-argument user functions never received the 2nd argument onward** (the call site
# **R0–R3 寄存器**取参,两套 ABI 对不上,`def f(a,b): return b` 调 `f(3,4)` 返回寄存器
# pushed arguments on the stack while the callee prologue read **registers R0–R3**) ⇒ now a
# 残留)⇒ 现在**形参就是调用方压好的那格**,0/1/N 参全对。
# parameter *is* the slot the caller pushed, so 0/1/N-argument calls are all correct.
#
# ③ **函数名与库符号重名会被库静默接管**(`def add(a, b): return a - b` 之后 `add(3, 4)`
# ③ **A function whose name collides with a library symbol is silently taken over by the library**
# 仍是 **7**,因为 `Lib/python/shared.py` 里也有 `add`,链接期库版本赢了)—— **这条没修**,
# (after `def add(a, b): return a - b`, `add(3, 4)` still returns **7** because
# 本程序仍然一个 `def` 都不写,也就没有重名风险。
# `Lib/python/shared.py` also defines `add`) — **this one is NOT fixed**; this file still
# writes no `def` at all, so it has no collision risk.
# ↳ 2026-10-04 复验仍成立:实测 `def add(a, b): return a - b` 之后 `add(3, 4)` 仍得 **7**(库版本赢)。
# ↳ Still true, re-checked 2026-10-04: after `def add(a, b): return a - b`, `add(3, 4)` still returns **7** (the library version wins).
#
# 另外两条**语言级**的限制**仍在**(本轮没动),它们直接决定了下面怎么写:
# Two more **language-level** limits **remain** (untouched this round); they shape the code below:
#
# · **没有 int → 字符串**(`str(x)` 是恒等,把整数当字符串指针传给宿主 ⇒ 画出来是空的)。
# · **There is no int → string** (`str(x)` is the identity, so the integer is handed to the host as a
# 分数/关卡/命数要显示就得**自己拆位再查字形表**(见 `DG` / `PWT`,这是本文件里唯一
# string pointer ⇒ it draws as nothing). Score / level / lives must be **split into digits and
# 有点绕的地方)。`chr(n)` 也不行(返回栈上临时缓冲的地址)。
# looked up in a glyph table** (see `DG` / `PWT`; that is the only convoluted part of this file).
# `chr(n)` is no help either — it returns the address of a stack temporary.
#
# · **浮点没有装箱**:`1.5 + 2.0` 在栈上串成位型(实测 1080033280),整数除法 `7 / 2` 得 3。
# · **Floats are not boxed**: `1.5 + 2.0` turns into a bit pattern on the stack (measured
# 所以球速用**整数定标**:位置存 1/4 像素、速度是"每帧多少 1/4 像素",全程整数。
# 1080033280) and `7 / 2` is 3. So the ball speed is **integer-scaled**: positions are stored in
# quarter-pixels and velocities are "quarter-pixels per frame". All integer, no floats anywhere.
#
# ## 操作
# ## Controls
#
# 开窗声明了 `VML_WIN_PORTRAIT`(坐标空间锁竖屏)+ `VML_WIN_NO_GAMEPAD`(不要手柄区 ⇒ 画布吃满整屏),
# The window declares `VML_WIN_PORTRAIT` (coordinate space locked to portrait) plus `VML_WIN_NO_GAMEPAD`
# 另配 `ui_scr_lock_orientation(VML_LOCK_PORTRAIT)` 让**系统**也不转屏(那是"系统让不让转",与"窗口跟不跟"是两件事)。
# (no gamepad area ⇒ the canvas fills the screen), and calls `ui_scr_lock_orientation(VML_LOCK_PORTRAIT)` so the
# **system** will not rotate either ("does the system allow rotation" and "does the window follow it"
# are two different things).
#
# **触摸**:手指按在哪里,挡板就跟到哪里(横向拖动)。
# **Touch**: wherever the finger is, the paddle is (drag horizontally).
# **键盘兜底**:←/→ 或 A/D 移动,空格/回车发球或重开,ESC 退出。
# **Keyboard fallback**: ←/→ or A/D to move, Space/Enter to serve or restart, ESC to quit.
#
# ## 跑法
# ## How to run
#
# 手机:`vml run examples/python/breakout.py`
# Phone: `vml run examples/python/breakout.py`
# 桌面:`vmlcli Examples/python/breakout.py --screen 411x726 --frame /tmp/b.png`
# Desktop: `vmlcli Examples/python/breakout.py --screen 411x726 --frame /tmp/b.png`
# 喂输入:`--input 脚本`(一行一条,`<毫秒> touchdown <x> <y>` / `<毫秒> keydown <键码>`)
# Feed input: `--input script` (one entry per line, `<ms> touchdown <x> <y>` / `<ms> keydown <key>`)
# ── 窗口:开窗时传 0(尺寸交给宿主),**开窗之后**再量一次真实画布 ──────────
# ── window: open with 0 (the host decides the size), measure the real canvas **after** it opens ──
# ── 显式引用:下面用到的 `ui_*` 全部来自共享库 vmlui.vml ────────────
# ── explicit reference: every `ui_*` below comes from the shared library vmlui.vml ──
# `Lib/python/vml_ui.py` 是 GenLib 从 `Lib/shared/src/vmlui.c` 生成的声明文件(勿手写),
# `Lib/python/vml_ui.py` is the declaration file GenLib generates from `Lib/shared/src/vmlui.c` (do not hand-write);
# 前端读它登记名字与形参个数 —— 有了声明表,实参个数写错会在**编译期**报错。
# the frontend reads it to register the names and their arities — with that table a wrong argument count is a **compile-time** error.
import vml_ui
LANG = ui_scr_get_language()
ui_scr_lock_orientation(VML_LOCK_PORTRAIT) # VML_LOCK_PORTRAIT —— 系统层面锁竖屏
# VML_LOCK_PORTRAIT — lock the orientation at the system level, not just in the window
# 开窗时传 0:尺寸交给宿主(用真实可用区)。**开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值。
# Open with 0: the host decides the size (the real usable area). Ask the **real** canvas only after
# the window is open -- before that the host can only estimate.
ui_win_open_ex("打砖块" if LANG == 0 else "Breakout", 0, 0, VML_WIN_PORTRAIT, VML_WIN_NO_GAMEPAD)
ui_scr_set_keep_on(1)
SW = ui_scr_get_w()
SH = ui_scr_get_h()
if SW <= 0:
SW = 411
if SH <= 0:
SH = 726
# ── 颜色(真彩 0xAARRGGBB,不是索引色)────────────────────────────────
# ── colors (true color 0xAARRGGBB, not indexed) ────────────────────────
C_BG = 0xFF0B1020
C_TEXT = 0xFFE8EAF2
C_DIM = 0xFF9AA0B0
C_PADDLE = 0xFF5AC8FA
C_BALL = 0xFFFFE066
C_WALL = 0xFF232A3E
C_MISS = 0xFFFF6B6B
C_OK = 0xFF4ADE80
C_WARN = 0xFFF0B429
# 砖块按行配色(7 档,行数最多 7)
# brick color per row (7 entries; at most 7 rows)
BRC = [0xFFE5555B, 0xFFE59A3C, 0xFFE8D44D, 0xFF4CC46B, 0xFF4A90D9, 0xFF9B59B6, 0xFF2AA9A0]
# 数字字形表 + 位权表 —— 本前端没有 int→字符串,数字只能自己拆位再查表(见文件头)
# digit glyph table + place-value table — this frontend has no int→string, so numbers are split
# into digits and looked up (see the file header)
DG = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", " "]
PWT = [10000, 1000, 100, 10, 1]
PWL = [10, 1]
# ── 场地几何(全部由 SW / SH 推出来,没有一个写死的画布尺寸)────────────
# ── field geometry (all derived from SW / SH; not one hard-coded canvas size) ──
HUD_H = 58
BRCOLS = 8
BW = SW // BRCOLS # 一块砖的宽
# width of one brick
BH = 20 # 一块砖的高
# height of one brick
BRICK_TOP = HUD_H + 10
BRROWS = 5 # 初始行数(过关 +1,上限 7)
# starting rows (one more per cleared level, capped at 7)
MAXROWS = 7
BRICK_N = MAXROWS * BRCOLS
PADH = 13
PADW = SW // 4
if PADW < 72:
PADW = 72
PADY = SH - 78
BR = 6 # 球半径
# ball radius
SCALE = 4 # 位置定标:1 像素 = 4 个整数单位
# position scale: 1 pixel = 4 integer units
SPD0 = 13 # 初速(单位/帧),约 3.2 像素/帧
# initial speed in units/frame, about 3.2 pixels/frame
# 砖块存活表:1 = 还在。**平铺**程序里模块级列表是可读可写的(函数里才看不见)。
# Brick aliveness table: 1 = still there. In a **flat** program a module-level list is readable and
# ⚠ 本前端的**列表字面量不能跨行**(`[1, 2,` 换行继续会被报 `意外的 token: NEWLINE`,
# writable (only function bodies could not see it). ⚠ A **list literal cannot span lines** in this
# 括号里也没有隐式续行)—— 所以下面这一行长 56 个 0,是有意写成一行的。
# done on one line.
# frontend (`[1, 2,` continued on the next line reports `意外的 token: NEWLINE`; there is no
# implicit continuation inside brackets) — so the line below is 56 zeros on purpose, all on one line.
# 括号里也没有隐式续行)—— 所以下面这一行长 56 个 0,是有意写成一行的。
# single long line of 56 zeros below.
BRS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# ── 局面状态 ──────────────────────────────────────────────────────────
# ── game state ────────────────────────────────────────────────────────
SCORE = 0
LEVEL = 1
LIVES = 3
ROWS = BRROWS
LEFT = 0 # 还剩几块砖
# bricks left
OVER = 0 # 0 进行中 1 通关 2 结束
# 0 = playing, 1 = won, 2 = game over
SERVE = 1 # 1 = 球还挂在挡板上没发出去
# 1 = the ball is still stuck to the paddle, not served yet
EVT = 0 # 最近一次事件(画在 HUD 上,既是反馈也是自检的取证点)
# last event (drawn on the HUD: both feedback and a self-test hook)
EVTT = 0 # 事件文字还要显示几帧
# how many more frames the event text stays on screen
BVX = 0
BVY = 0
BXS = 0
BYS = 0
PADX = 0
DOWN0 = 0 # 上一帧手指是否按着(用来做"点一下"的边沿检测)
# was the finger down last frame (edge detection for a single tap)
# 事件码 → 文案。本前端没有字符串拼接,只能两套字面量按语言选。
# event code → text. This frontend has no string concatenation, so both languages are spelled out.
E_NONE = 0
E_PADDLE = 1
E_BRICK = 2
E_WALL = 3
E_MISS = 4
E_CLEAR = 5
E_OVER = 6
# ── 铺砖 ──────────────────────────────────────────────────────────────
# ── lay the bricks ────────────────────────────────────────────────────
i = 0
while i < BRICK_N:
BRS[i] = 0
i = i + 1
r = 0
while r < ROWS:
c = 0
while c < BRCOLS:
BRS[r * BRCOLS + c] = 1
c = c + 1
r = r + 1
LEFT = ROWS * BRCOLS
PADX = (SW - PADW) // 2
# 定时器:16ms 一拍 —— 球速按**拍**算(拍由这个定时器按**墙钟**走,与渲染帧率无关)。
# timer: one tick every 16 ms -- the ball speed is **per tick** (ticks run on the wall clock, independent of the render frame rate).
# ⚠ 2026-10-04 起**帧节流归宿主**(`ui_2d_present()` 之后按显示预算走);这一句只决定
# ⚠ Since 2026-10-04 the host owns frame pacing (it paces after `ui_2d_present()` by display budget); this line only sets
# "逻辑多久推进一次",**不是**帧节流。
# how often the logic advances -- it is NOT frame pacing.
TICKMS = 16 # 逻辑一拍 = 16ms(墙钟口径,见主循环里的补步)
# one logic tick = 16 ms (wall-clock basis; see the catch-up in the main loop)
TID = ui_timer_set(TICKMS, 1)
# 墙钟补步累加器(按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数)
# Wall-clock catch-up accumulator (steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds)
ACC = 0
LASTMS = ui_time_get_ms() # 墙钟起点(进主循环时取)
# wall-clock origin, sampled when entering the main loop
# 音效:**单通道** —— 一次事件只发一个音(`ui_audio_beep` 的老语义就是这样),所以下面每个
# Sound: **single channel** — one event makes one beep (`ui_audio_beep` has always worked that way), so
# 事件分支里至多调一次。频率是刻意区分的:撞墙 300Hz / 挡板 520Hz / 砖块 660Hz /
# every event branch below calls it at most once. The frequencies are chosen on purpose: wall 300Hz,
# 发球 880Hz / 过关 990Hz / 掉球 160Hz —— 它同时是自检取证用的判据。
# paddle 520Hz, brick 660Hz, serve 880Hz, level cleared 990Hz, ball lost 160Hz — they double as a
# self-test hook.
# 发球 880Hz / 过关 990Hz / 掉球 160Hz —— 它同时是自检取证用的判据。
# point for automated checks.
# ── 主循环:定时器驱动,触摸与键盘同一个队列 ────────────────────────────
# ── main loop: driven by the timer; touches and keys share one queue ────
while ui_win_is_closed() == 0:
# 触摸是"追最新位置"的语义 ⇒ 每帧把积压的触摸消息丢掉,直接轮询当前值。
# Touches mean "follow the newest position": drop the backlog each frame and poll the current value.
# 不丢的话拖动时队列只涨不落(实测能涨到上千条),触摸会排队等半天才反应。
# measured) and touches take ages to be seen.
ui_msg_drop(1) # VML_MSG_KIND_TOUCH
# 按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数。
# steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds.
# 定时器只做唤醒:真实 9 号消息**不再直接推进**,跑几帧由这里欠账决定;
# the timer only wakes us up: the real message #9 **no longer advances anything**; how many frames to run is decided by the deficit here;
# 欠账时**不再等消息**,立刻补跑一帧。
# while in deficit it **does not wait for a message** and runs one frame immediately.
NOW = ui_time_get_ms()
ACC = ACC + (NOW - LASTMS)
LASTMS = NOW
if ACC > TICKMS * 8:
ACC = 0 # 卡太久别一次补一大串
# do not catch up in one huge burst after a long stall
t = 0
if ACC < TICKMS:
t = ui_msg_fetch_wait(0) # 欠账还清才回去等
# go back to waiting only once the deficit is cleared
if t == 10: # WindowClose
break
if t == 1: # KeyDown
k = ui_msg_get_a()
if k == 27: # ESC 退出
# ESC quits
break
if k == 13 or k == 32: # 回车 / 空格
# Enter / Space
if SERVE == 1 and OVER == 0:
SERVE = 0
BVX = 0
BVY = 0 - SPD0 - (LEVEL - 1) * 2
ui_audio_beep(880, 55)
elif OVER != 0:
# 重开
# restart
SCORE = 0
LEVEL = 1
LIVES = 3
ROWS = BRROWS
i = 0
while i < BRICK_N:
BRS[i] = 0
i = i + 1
r = 0
while r < ROWS:
c = 0
while c < BRCOLS:
BRS[r * BRCOLS + c] = 1
c = c + 1
r = r + 1
LEFT = ROWS * BRCOLS
OVER = 0
SERVE = 1
EVT = E_NONE
# Timer —— 这一拍是"一帧":推进物理再画(跑几帧由上面的「按墙钟补步」欠账决定)
# Timer — one tick is one "frame": advance the physics, then draw
# (how many frames run is decided by the wall-clock debt above)
if ACC >= TICKMS:
ACC = ACC - TICKMS
# ── 输入 ──
# ── input ──
# 触摸优先:手指在哪,挡板中心就在哪
# Touch wins: the paddle centre goes wherever the finger is
if ui_touch_fetch(0) == 1:
if ui_touch_is_down() == 1:
tx = ui_touch_get_x()
PADX = tx - PADW // 2
if PADX < 0:
PADX = 0
if PADX > SW - PADW:
PADX = SW - PADW
if SERVE == 1:
# 发球前拖着挡板走,球跟着挂在挡板上
# while serving, dragging the paddle carries the ball with it
BXS = (PADX + PADW // 2) * SCALE
BYS = (PADY - BR - 2) * SCALE
# 键盘兜底:按住 ←/→ 或 A/D 时持续移动(连发的刹车是"松手就不再移动"这条本身)
# keyboard fallback: keep moving while ←/→ or A/D is held (releasing the key is the brake)
if ui_key_is_down(37) == 1 or ui_key_is_down(65) == 1:
PADX = PADX - 14
if ui_key_is_down(39) == 1 or ui_key_is_down(68) == 1:
PADX = PADX + 14
if PADX < 0:
PADX = 0
if PADX > SW - PADW:
PADX = SW - PADW
# 发球前球挂在挡板上
# before it is served the ball rides on the paddle
if SERVE == 1:
BXS = (PADX + PADW // 2) * SCALE
BYS = (PADY - BR - 2) * SCALE
# 点一下屏幕也能发球(手指抬起后再按下的那一沿)
# a tap on the screen also serves (the edge where the finger comes back down)
if ui_touch_is_down() == 1 and DOWN0 == 0:
SERVE = 0
BVX = 0
BVY = 0 - SPD0 - (LEVEL - 1) * 2
ui_audio_beep(880, 55)
# ── 物理 ──
# ── physics ──
if OVER == 0 and SERVE == 0:
OLDX = BXS
OLDY = BYS
BXS = BXS + BVX
BYS = BYS + BVY
BX = BXS // SCALE
BY = BYS // SCALE
# 左右墙
# side walls
if BX - BR < 0:
BXS = BR * SCALE
BVX = 0 - BVX
EVT = E_WALL
EVTT = 8
ui_audio_beep(300, 25)
if BX + BR > SW:
BXS = (SW - BR) * SCALE
BVX = 0 - BVX
EVT = E_WALL
EVTT = 8
ui_audio_beep(300, 25)
# 顶墙
# top wall
if BY - BR < HUD_H - 6:
BYS = (HUD_H - 6 + BR) * SCALE
BVY = 0 - BVY
EVT = E_WALL
EVTT = 8
ui_audio_beep(300, 25)
BX = BXS // SCALE
BY = BYS // SCALE
PBX = OLDX // SCALE
PBY = OLDY // SCALE
# 砖块:用球心所在格判定;反弹方向用"上一帧在哪一行"分辨横竖
# bricks: judged by the cell the ball centre is in; the axis comes from the row it was in last frame
if BY >= BRICK_TOP:
RX = BX // BW
RY = (BY - BRICK_TOP) // BH
if RX >= 0 and RX < BRCOLS and RY >= 0 and RY < MAXROWS:
BI = RY * BRCOLS + RX
if BRS[BI] == 1:
BRS[BI] = 0
LEFT = LEFT - 1
SCORE = SCORE + 10 * LEVEL
EVT = E_BRICK
EVTT = 12
ui_audio_beep(660, 30)
PRY = (PBY - BRICK_TOP) // BH
if PRY != RY:
BVY = 0 - BVY
BYS = OLDY
else:
BVX = 0 - BVX
BXS = OLDX
# 挡板:只在球往下走、且落到挡板上沿那一带时才判定
# paddle: only when the ball is moving down and reaches the band at the paddle's top edge
if BVY > 0 and BY + BR >= PADY and PBY + BR <= PADY + 6:
if BX >= PADX - BR and BX <= PADX + PADW + BR:
# 打在挡板哪儿,决定反弹角度:越靠边越斜
# where it hit the paddle sets the bounce angle: the further out, the steeper
HITX = BX - (PADX + PADW // 2)
SP = SPD0 + (LEVEL - 1) * 2
BVX = HITX * SP // (PADW // 2)
if BVX > SP:
BVX = SP
if BVX < 0 - SP:
BVX = 0 - SP
# 别让球变成近乎垂直(否则会在一条线上无限来回)—— 留一点横向分量
# keep a little horizontal component so the ball never bounces straight up and down forever
if BVX == 0:
BVX = 2
BVY = 0 - SP
BYS = (PADY - BR) * SCALE
EVT = E_PADDLE
EVTT = 18
ui_audio_beep(520, 35)
# 掉出屏幕:丢一条命
# fell off the bottom: lose a life
if BY - BR > SH:
LIVES = LIVES - 1
EVT = E_MISS
EVTT = 30
ui_audio_beep(160, 140)
if LIVES <= 0:
OVER = 2
EVT = E_OVER
EVTT = 999
else:
SERVE = 1
BVX = 0
BVY = 0
BXS = (PADX + PADW // 2) * SCALE
BYS = (PADY - BR - 2) * SCALE
# 过关:砖块清空
# level cleared: no bricks left
if LEFT <= 0 and OVER == 0:
LEVEL = LEVEL + 1
if LEVEL > 8:
LEVEL = 8
ROWS = BRROWS + LEVEL - 1
if ROWS > MAXROWS:
ROWS = MAXROWS
i = 0
while i < BRICK_N:
BRS[i] = 0
i = i + 1
r = 0
while r < ROWS:
c = 0
while c < BRCOLS:
BRS[r * BRCOLS + c] = 1
c = c + 1
r = r + 1
LEFT = ROWS * BRCOLS
SERVE = 1
BVX = 0
BVY = 0
EVT = E_CLEAR
EVTT = 60
ui_audio_beep(990, 40)
if EVTT > 0:
EVTT = EVTT - 1
if ui_touch_is_down() == 1:
DOWN0 = 1
else:
DOWN0 = 0
# ── 画这一帧 ──
# ── draw this frame ──
ui_2d_clear(C_BG)
# 砖块
# bricks
r = 0
while r < ROWS:
c = 0
while c < BRCOLS:
if BRS[r * BRCOLS + c] == 1:
ui_2d_rect(c * BW + 2, BRICK_TOP + r * BH + 2, BW - 4, BH - 4, BRC[r], 1, 0, 4)
c = c + 1
r = r + 1
# 球与挡板
# ball and paddle
ui_2d_rect(PADX, PADY, PADW, PADH, C_PADDLE, 1, 0, 6)
ui_2d_rect(PADX + 4, PADY + 3, PADW - 8, 3, 0xFFFFFFFF, 1, 0, 1)
ui_2d_circle(BXS // SCALE, BYS // SCALE, BR, C_BALL, 1, 0)
ui_2d_circle(BXS // SCALE - 2, BYS // SCALE - 2, 2, 0xFFFFFFFF, 1, 0)
# 顶栏:分数 / 关卡 / 命数
# HUD: score / level / lives
ui_text_set_font(13, 0, C_DIM, 0)
ui_2d_text_cur(10, 10, "分数" if LANG == 0 else "SCORE")
ui_2d_text_cur(SW - 12 - 96, 10, "关卡" if LANG == 0 else "LEVEL")
ui_2d_text_cur(10, 34, "命" if LANG == 0 else "LIVES")
ui_text_set_font(16, 0, C_TEXT, 0)
sx = 10 + 42
i = 0
st = 0
while i < 5:
d = (SCORE // PWT[i]) % 10
if d != 0 or st == 1 or i == 4:
st = 1
ui_2d_text_cur(sx, 7, DG[d])
sx = sx + 12
i = i + 1
sx = SW - 12 - 96 + 44
i = 0
st = 0
while i < 2:
d = (LEVEL // PWL[i]) % 10
if d != 0 or st == 1 or i == 1:
st = 1
ui_2d_text_cur(sx, 7, DG[d])
sx = sx + 12
i = i + 1
# 命数画成小圆点(比数字直观,也绕开了"没有 int→字符串")
# lives are drawn as dots (clearer than a number, and it sidesteps "no int to string")
i = 0
while i < LIVES:
ui_2d_circle(10 + 24 + i * 20, 42, 7, C_OK, 1, 0)
i = i + 1
# 事件反馈行:**闪一下**(就是文件头说的那条"既能玩、又能取证"的地方)。
# Event feedback line: it **flashes** (the "playable and forensically useful" spot from the header).
# ⚠ 必须连 `EVTT > 0` 一起判 —— 只看 `EVT` 的话最后那一条会**一直挂在屏幕上**
# ⚠ The `EVTT > 0` test is part of the condition — keying off `EVT` alone leaves the last event
# (`EVT` 只在事件发生时改,闪烁靠 `EVTT` 倒计时),那就成了"状态行"而不是"反馈"。
# **stuck on screen forever** (`EVT` only changes when an event happens; the blink comes from the
# `EVTT` countdown), which makes it a status line instead of feedback.
# (`EVT` 只在事件发生时改,闪烁靠 `EVTT` 倒计时),那就成了"状态行"而不是"反馈"。
# ⚠ The flash must also test `EVTT > 0`: gating on `EVT` alone leaves the last message
if EVTT > 0:
if EVT == E_PADDLE:
ui_text_set_font(14, 0, C_PADDLE, 1)
ui_2d_text_cur(SW // 2, SH - 40, "击中挡板!" if LANG == 0 else "PADDLE!")
if EVT == E_BRICK:
ui_text_set_font(14, 0, C_WARN, 1)
ui_2d_text_cur(SW // 2, SH - 40, "打碎砖块!" if LANG == 0 else "BRICK!")
if EVT == E_WALL:
ui_text_set_font(14, 0, C_DIM, 1)
ui_2d_text_cur(SW // 2, SH - 40, "撞墙" if LANG == 0 else "WALL")
if EVT == E_MISS:
ui_text_set_font(16, 3, C_MISS, 1)
ui_2d_text_cur(SW // 2, SH - 40, "掉球了!" if LANG == 0 else "MISSED!")
if EVT == E_CLEAR:
ui_text_set_font(16, 3, C_OK, 1)
ui_2d_text_cur(SW // 2, SH - 40, "过关!" if LANG == 0 else "LEVEL CLEAR!")
if SERVE == 1 and OVER == 0:
ui_text_set_font(13, 0, C_DIM, 1)
ui_2d_text_cur(SW // 2, SH - 20, "点一下发球 / 空格发球" if LANG == 0 else "Tap or Space to serve")
if OVER == 2:
ui_2d_rect(SW // 6, SH // 2 - 60, SW * 2 // 3, 130, 0xFF161B2E, 1, 0, 10)
ui_text_set_font(24, 3, C_MISS, 1)
ui_2d_text_cur(SW // 2, SH // 2 - 30, "游戏结束" if LANG == 0 else "GAME OVER")
ui_text_set_font(14, 0, C_TEXT, 1)
ui_2d_text_cur(SW // 2, SH // 2, "点屏幕或按空格重开" if LANG == 0 else "Tap / Space to restart")
if OVER == 1:
ui_text_set_font(24, 3, C_OK, 1)
ui_2d_text_cur(SW // 2, SH // 2, "全部通过!" if LANG == 0 else "ALL CLEAR!")
# ⚠ 每帧末尾:`ui_2d_present()` 是"这一帧画完了"的唯一信号
# ⚠ at the end of every frame: `ui_2d_present()` is the only "this frame is done" signal
ui_2d_present()
# ── 收尾:定时器要杀,屏幕常亮要还回去 ──
# ── wrap-up: kill the timer, give screen-on back ──
ui_timer_kill(TID)
ui_scr_set_keep_on(0)
ui_win_close()
print("breakout: done score=", SCORE, " level=", LEVEL, " left=", LEFT, " lives=", LIVES)