← 都来码 / Dolaima← 例程索引python/app/breakout.py原文

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)