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

python/app/tetris.py · 18539 字节 · 下载原文

# tetris.py —— 俄罗斯方块(VML 的 Python 前端)

# tetris.py — Tetris (VML's Python frontend)

#

# 界面走**共享调用库** `Lib/shared/vmlui.c`(编成 `vmlui.vml`,由 vmltool.config.xml 挂到

# The UI goes through the **shared call library** `Lib/shared/vmlui.c` (compiled into `vmlui.vml` and attached to

# python 的 Libs 上):Python 前端不支持内联 asm(),只能按标签调 C 函数 —— 这正是那座共享库

# python's Libs by vmltool.config.xml): the Python frontend does not support inline asm(), so it can only call C functions

# 存在的意义。

# by label — which is exactly the point of that shared library existing.

#

# ⚠ 两条**实测出来的前端限制**,本程序是绕开它们写的:

# ⚠ Two **measured frontend limits** that this program is written to work around:

#   1. ~~**列表不能写**:`b[i] = v` 之后读回来还是 0(字面量列表读是对的,写不生效)~~ ✅ **平铺写已修好(2026-10-04 复验)**:

#   1. ~~**Lists cannot be written**: after `b[i] = v` reading it back is still 0~~ ✅ **flat writes are fixed (re-checked 2026-10-04)**:

#      实测 `b[1] = 7` 读回 7、`b[i] = 9` 读回 9(常量/变量下标都对);**嵌套列表写仍不生效**(`g[0][1] = 5` 读回仍是 0)。

#      measured `b[1] = 7` reads 7 and `b[i] = 9` reads 9 (constant or variable subscript); **nested list writes still have no effect** (`g[0][1] = 5` still reads 0).

#      棋盘是二维的,所以仍不放 Python 里,改用共享库的整数网格 `ui_grid_set/ui_grid_get`。

#      The board is 2-D, so it still does not live in Python; it uses the shared library's integer grid `ui_grid_set/ui_grid_get`.

#      (能跑就别动;新写的代码里**平铺**列表直接写即可,只有嵌套列表还得绕。)

#      (Existing code is fine as is; new code may write **flat** lists directly -- only nested lists still need the workaround.)

#   2. 没有 asm()(`PythonCompiler/CodeGenerator.Expressions.cs:154` 注明仅 C/ObjC/C++)。

#   2. There is no asm() (`PythonCompiler/CodeGenerator.Expressions.cs:154` notes it is C/ObjC/C++ only).

#      ↳ 2026-10-04 复验仍成立:`asm("SYSCALL 6")` 实测 `error: 未定义的函数 'asm'(引用 2 次)`。

#      ↳ Still true, re-checked 2026-10-04: `asm("SYSCALL 6")` measures `error: undefined function 'asm' (referenced 2 times)`.

#

# 操作(触摸):点屏幕左 1/3 左移、右 1/3 右移、中间**旋转**;点最下面那条**直落到底**。

# Controls (touch): tap the left 1/3 of the screen to move left, the right 1/3 to move right, the middle to **rotate**; tap the bottom strip to **drop all the way down**.

# 返回箭头退出。

# The back arrow exits.


# ── 常量 ───────────────────────────────────────────────

# ── constants ───────────────────────────────────────────

# ── 显式引用:下面用到的 `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

W = 10
H = 20
TICK = 450          # 下落一格的间隔(毫秒)。⚠ **走定时器**,不是靠等待超时 ——

                    # one drop every 450 ms. ⚠ It **uses the timer**, never a wait timeout --

                    # 见下方 ui_timer_set 那段(用户 2026-10-04:帧率不该是玩法的一部分)

                    # see the ui_timer_set block below (user 2026-10-04: the frame rate must not be part of the gameplay)


C_BG = 0xFF101018
C_GRID = 0xFF2A2A38
C_WALL = 0xFF3A3A4C
C_TEXT = 0xFFEDEDF2
C_SCORE = 0xFF4ADE80

# 7 种方块:4×4 位掩码(行优先,第 r 行第 c 位 = r*4+c)

# 7 piece types: 4×4 bitmasks (row-major, bit for row r column c = r*4+c)

# 旋转用位运算现场算,不存 28 张表(前端列表不可靠,少存一份少一个坑)

# Rotation is computed on the fly with bit operations instead of storing 28 tables (frontend lists are unreliable; one less table is one less pitfall)

P_O = 0x0066
P_I = 0x0F00
P_S = 0x006C
P_Z = 0x00C6
P_T = 0x00E4
P_L = 0x0062
P_J = 0x00E8

# 棋盘/掩码在共享库网格里的布局

# Layout of the board/masks inside the shared library grid

BOARD_AT = 0
MASK_AT = 200

# 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。

# UI language: ask the host once at startup whether Chinese or English is wanted (0=Chinese 1=English), then branch on it for the whole game.

# ⚠ 别在每帧里调 —— 那是一次 syscall。

# ⚠ Do not call it every frame — it is a syscall.

# ⚠ **不能放模块级全局**(`LANG = ui_scr_get_language()` 在函数里读出来恒 0 —— 本前端的

# ⚠ **It cannot be a module-level global** (`LANG = ui_scr_get_language()` always reads back 0 inside a function — in this frontend

#   模块级标量在函数体内不可见,实测;本文件本来就为此把 score/over 一路当参数传)。

#   a module-level scalar is invisible inside a function body, measured; this file already passes score/over around as parameters for that reason).

#   所以走**参数**:main 里问一次,`draw(...)` 多收一个 `lang`。

#   So it travels as a **parameter**: ask once in main, and `draw(...)` takes one extra `lang`.

#   界面文字一律写成 `"中文" if lang == 0 else "English"`(本前端支持三元,见语言规范 §5.9)。

#   UI text is always written as `"Chinese" if lang == 0 else "English"` (this frontend supports the ternary, see language spec §5.9).


# ── 共享库网格上的棋盘读写 ──────────────────────────────

# ── board reads/writes on the shared library grid ──────────────────

def bget(x, y):
    if x < 0 or x >= W or y < 0 or y >= H:
        return 1              # 墙:越界一律当"有块",碰撞判定就不用再判边界

        # wall: anything out of bounds counts as "occupied", so collision checks need no boundary test

    return ui_grid_get(BOARD_AT + y * W + x)

def bset(x, y, v):
    if x >= 0 and x < W and y >= 0 and y < H:
        ui_grid_set(BOARD_AT + y * W + x, v)

def board_clear():
    i = 0
    while i < W * H:
        ui_grid_set(BOARD_AT + i, 0)
        i = i + 1

# ── 方块:某块的某次旋转,取它的 4 个格子的坐标 ──────────

# ── pieces: the coordinates of the 4 cells of a given piece in a given rotation ──────

# 旋转 4×4 掩码 90°:位 (r,c) → (c, 3-r)

# Rotate a 4×4 mask by 90°: bit (r,c) → (c, 3-r)

def rot90(m):
    out = 0
    r = 0
    while r < 4:
        c = 0
        while c < 4:
            if (m >> (r * 4 + c)) & 1:
                out = out | (1 << (c * 4 + (3 - r)))
            c = c + 1
        r = r + 1
    return out

def piece_mask(pid, rot):
    m = ui_grid_get(MASK_AT + pid)
    k = 0
    while k < rot:
        m = rot90(m)
        k = k + 1
    return m

# 判断某块在 (px,py) 处是否与已有块/墙冲突

# Whether the piece at (px,py) collides with an existing block / wall

def collide(pid, rot, px, py):
    m = piece_mask(pid, rot)
    r = 0
    while r < 4:
        c = 0
        while c < 4:
            if (m >> (r * 4 + c)) & 1:
                if bget(px + c, py + r) != 0:
                    return 1
            c = c + 1
        r = r + 1
    return 0

# 把方块固定进棋盘

# Lock the piece into the board

def lock_piece(pid, rot, px, py):
    m = piece_mask(pid, rot)
    r = 0
    while r < 4:
        c = 0
        while c < 4:
            if (m >> (r * 4 + c)) & 1:
                bset(px + c, py + r, 1)
            c = c + 1
        r = r + 1

# 消行:返回消掉的行数

# Clear lines: returns the number of lines cleared

def clear_lines():
    n = 0
    y = H - 1
    while y >= 0:
        full = 1
        x = 0
        while x < W:
            if bget(x, y) == 0:
                full = 0
            x = x + 1
        if full == 1:
            n = n + 1
            # 上面的整体下移一行

            # Shift the whole thing above down by one row

            yy = y
            while yy > 0:
                x = 0
                while x < W:
                    bset(x, yy, bget(x, yy - 1))
                    x = x + 1
                yy = yy - 1
            x = 0
            while x < W:
                bset(x, 0, 0)
                x = x + 1
            # 就地重判同一行(新落下来的那行)

            # Re-check the same row in place (the row that just fell into it)

        else:
            y = y - 1
    return n

# ── 绘制 ──────────────────────────────────────────────

# ── drawing ──────────────────────────────────────────

def draw(px, py, rot, pid, score, over, lang):
    sw = ui_scr_get_w()
    sh = ui_scr_get_h()
    if sw <= 0:
        sw = 360
    if sh <= 0:
        sh = 620

    # 棋盘按可用高度铺满,宽度按 1:2 比例

    # Fill the board to the available height, with the width at a 1:2 ratio

    area = sh - 70
    cell = area / H
    if cell * W > sw - 20:
        cell = (sw - 20) / W
    bw = cell * W
    bh = cell * H
    ox = (sw - bw) / 2
    oy = 44

    ui_2d_clear(C_BG)

    # 棋盘底

    # Board background

    ui_2d_rect(ox, oy, bw, bh, C_WALL, 1, 0, 4)

    # 网格

    # Grid

    i = 1
    while i < W:
        ui_2d_line(ox + i * cell, oy, ox + i * cell, oy + bh, C_GRID, 1)
        i = i + 1
    i = 1
    while i < H:
        ui_2d_line(ox, oy + i * cell, ox + bw, oy + i * cell, C_GRID, 1)
        i = i + 1

    # 已固定的块

    # Already locked blocks

    y = 0
    while y < H:
        x = 0
        while x < W:
            if bget(x, y) != 0:
                ui_2d_rect(ox + x * cell + 1, oy + y * cell + 1, cell - 2, cell - 2, 0xFF58A6FF, 1, 0, 2)
            x = x + 1
        y = y + 1

    # 当前方块

    # Current piece

    m = piece_mask(pid, rot)
    r = 0
    while r < 4:
        c = 0
        while c < 4:
            if (m >> (r * 4 + c)) & 1:
                ui_2d_rect(ox + (px + c) * cell + 1, oy + (py + r) * cell + 1, cell - 2, cell - 2, 0xFFF0B429, 1, 0, 2)
            c = c + 1
        r = r + 1

    # 状态行

    # Status line

    ui_text_set_font(15, 1, C_TEXT, 0)
    ui_2d_text_cur(ox, 12, "俄罗斯方块" if lang == 0 else "Tetris")
    ui_text_set_font(14, 0, C_SCORE, 2)
    ui_2d_text_cur(ox + bw, 12, str(score))

    if over == 1:
        ui_text_set_font(20, 3, 0xFFFF6B6B, 0)
        ui_2d_text_cur(ox + bw / 2, oy + bh / 2, "游戏结束" if lang == 0 else "Game Over")
        ui_text_set_font(14, 0, C_TEXT, 0)
        ui_2d_text_cur(ox + bw / 2, oy + bh / 2 + 28, "点任意处重开" if lang == 0 else "Tap anywhere to restart")

    ui_2d_present()

# ── 主程序 ────────────────────────────────────────────

# ── main program ──────────────────────────────────────

def main():
    # 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),之后整局按它分支。

    # UI language: ask the host once at startup whether Chinese or English is wanted (0=Chinese 1=English), then branch on it for the whole game.

    # ⚠ 别在每帧里调 —— 那是一次 syscall;这里问一次,`draw` 收参数。

    # ⚠ Do not call it every frame — it is a syscall; ask once here, and `draw` takes it as a parameter.

    lang = ui_scr_get_language()

    # 把 7 种方块的基准掩码放进共享库网格(前端列表不可靠,数据也放这边)

    # Put the base masks of the 7 piece types into the shared library grid (frontend lists are unreliable, so the data lives there too)

    ui_grid_set(MASK_AT + 0, P_O)
    ui_grid_set(MASK_AT + 1, P_I)
    ui_grid_set(MASK_AT + 2, P_S)
    ui_grid_set(MASK_AT + 3, P_Z)
    ui_grid_set(MASK_AT + 4, P_T)
    ui_grid_set(MASK_AT + 5, P_L)
    ui_grid_set(MASK_AT + 6, P_J)

    # 开窗时传 0:尺寸交给宿主(用真实可用区)。`draw()` 与触摸判定每次都现问一次真实画布(见文件头)。

    # Open with 0 so the host decides the size (the real usable area). `draw()` and hit-testing

    # re-ask the real canvas every time (see the header).

    ui_win_open("俄罗斯方块" if lang == 0 else "Tetris", 0, 0)

    board_clear()

    score = 0
    over = 0
    pid = 0
    rot = 0
    px = 3
    py = 0

    ui_dlg_msg("俄罗斯方块" if lang == 0 else "Tetris", "点左边左移、右边右移、中间旋转;点最下面直落。" if lang == 0 else "Tap left/right to move, center to rotate, bottom to drop.", VML_DLG_INFO)

    # ⚠ 重力**必须走定时器**(用户 2026-10-04 的口径:「不应该是间隔多久绘制,

    # ⚠ Gravity **must use the timer** (the user's 2026-10-04 rule: "it should not be 'draw every N ms',

    # 而是空闲了就绘制」)。老写法把 ui_msg_fetch_wait(TICK) 的**超时**当成"下落一拍"

    # but 'draw whenever idle'"). The old code treated a ui_msg_fetch_wait(TICK) **timeout** as "one drop"

    # —— 那是把帧率当玩法:宿主改成一帧跟着显示走之后会变成每 ~16ms 掉一格,不可玩。

    # -- that makes the frame rate part of the gameplay: once the host paces to the display, the piece drops every ~16 ms and the game is unplayable.

    TID = ui_timer_set(TICK, 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


    draw(px, py, rot, pid, score, over, lang)

    while ui_win_is_closed() == 0:
        t = ui_msg_fetch_wait(TICK)

        if t == 10:            # WindowClose

            break

        # 按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数

        # steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds

        NOW = ui_time_get_ms()
        ACC = ACC + (NOW - LASTMS)
        LASTMS = NOW
        if ACC > TICK * 8:
            ACC = 0                # 卡太久别一次补一大串

                                   # do not catch up in one huge burst after a long stall

        while ACC >= TICK:
            ACC = ACC - TICK
            if over == 0:
                if collide(pid, rot, px, py + 1) == 0:
                    py = py + 1
                else:
                    lock_piece(pid, rot, px, py)
                    score = score + clear_lines() * 100
                    pid = (pid + 1) % 7
                    rot = 0
                    px = 3
                    py = 0
                    if collide(pid, rot, px, py) != 0:
                        over = 1

        if t == 6:             # TouchDown

            tx = ui_msg_get_a()
            ty = ui_msg_get_b()
            sw = ui_scr_get_w()
            sh = ui_scr_get_h()

            if over == 1:
                # 重开

                # Restart

                board_clear()
                score = 0
                over = 0
                pid = 0
                rot = 0
                px = 3
                py = 0
            elif ty > sh * 3 / 4:
                # 直落:一直往下直到碰住

                # Hard drop: keep going down until it hits something

                while collide(pid, rot, px, py + 1) == 0:
                    py = py + 1
                lock_piece(pid, rot, px, py)
                score = score + clear_lines() * 100
                pid = (pid + 1) % 7
                rot = 0
                px = 3
                py = 0
                if collide(pid, rot, px, py) != 0:
                    over = 1
            elif tx < sw / 3:
                if collide(pid, rot, px - 1, py) == 0:
                    px = px - 1
            elif tx > sw * 2 / 3:
                if collide(pid, rot, px + 1, py) == 0:
                    px = px + 1
            else:
                nr = (rot + 1) % 4
                if collide(pid, nr, px, py) == 0:
                    rot = nr

        else:
            # 其它消息(触摸/按键/**定时器唤醒**/等待超时):只重画,不动重力。

            # Any other message (touch/key/**timer wake**/timeout): just redraw, do NOT advance gravity.

            # 定时器只做唤醒(按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数),

            # The timer only wakes us up (steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds),

            # 重力步进在上面的墙钟补步块里,与帧率/拍数无关。

            # and the gravity step lives in the wall-clock catch-up block above, independent of the frame rate or tick count.

            pass

        draw(px, py, rot, pid, score, over, lang)

    ui_win_close()

main()