# 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()
