ruby/app/invaders.rb · 29193 字节 · 下载原文
# 太空侵略者 —— 用 **Ruby** 写的手机小游戏(实时动作 / 射击)
# Space Invaders — a small phone game written in **Ruby** (real-time action / shooter)
#
# 玩法:左右移动飞船,把一队外星人清掉就进下一关(每关队形更快)。被炸弹打中掉一条命,
# How to play: move the ship left and right; clear the whole formation to advance to the next level (each level is faster). A bomb hit costs one life,
# 命掉光或者外星人压到飞船那一行就结束。每打掉一个 +10 分,通关另有奖励分。
# and running out of lives — or letting the invaders reach the ship's row — ends the round. Each kill is +10, and clearing a level pays a bonus.
#
# ◆ 操作
# ◆ Controls
#
# 触摸:点哪儿飞船就去哪儿;炮是**自动补**的(上一发落地就再发一发)。
# Touch: tap anywhere and the ship goes there; the cannon **reloads by itself** (a new shot the moment the last one is gone).
# 键盘:← → 移动、空格 / ↑ / 回车开炮、R 重开、Esc 退出。
# Keyboard: left/right arrows move, Space / Up / Enter fires, R restarts, Esc quits.
#
# ◆⚠ 本前端最坑的一条(本文件同 `minesweeper.rb` 实测):**函数的写法决定"全局"能不能被看见**
# ◆⚠ The nastiest item on this frontend (measured in this file and `minesweeper.rb`): **where the `def` sits decides whether a "global" is visible at all**
#
# ⚠ **凡是函数要用的顶层变量,赋值必须写在所有 `def` 之后** —— 写在 `def` 之前的话,
# ⚠ **Every top-level variable a function uses must be assigned after every `def`** — if it is assigned before,
# 函数体里那个名字会被当成**未初始化的局部**,读到的是垃圾值(不是 0、也不是报错)。
# the name inside the function body is treated as an **uninitialised local**, and you read garbage (not zero, and no error).
# ⇒ 本文件因此把全部 `def` 放在最前面,第二段才是布局与状态(`g_` 打头那一批)。
# ⇒ This file therefore puts every `def` at the very top; layout and state (the `g_`-prefixed batch) come second.
#
# ⚠⚠ 还有**第二半**,更难看出来:函数体里**只写不读**的名字,写的是**该函数自己的局部**
# ⚠⚠ There is a **second half**, harder to spot: a name that is only **written** and never read inside a function body is written to **that function's own local**
# (顶层那个纹丝不动)。真身在 `CompilerBase/CodeGeneratorBase.cs` 的 `EmitStoreVar`:
# (the top-level one never changes). The cause is `EmitStoreVar` in `CompilerBase/CodeGeneratorBase.cs`:
# 局部表命中 → 全局槽 `var_<名>` 已存在 → **否则新开一个局部**;而全局槽只有**被函数读过**才会存在。
# local table hit -> the global slot `var_<name>` exists -> **otherwise allocate a fresh local**; and that global slot exists only once a function has **read** the name.
# 实测:`def set_only(); g_a = 99; end` 之后顶层 `print(g_a)` 仍是旧值。
# Measured: after `def set_only(); g_a = 99; end`, a top-level `print(g_a)` still shows the old value.
# ⇒ 对策是文件里那个 `iv_touch_state()`:把"各函数要写的状态名"**先读一遍**(它不参与游戏逻辑)。
# ⇒ The countermeasure is `iv_touch_state()` below: it **reads once** every state name the other functions write (it plays no part in the game logic).
#
# ◆ 本前端另外三条实测限制(决定了下面为什么这么写)
# ◆ Three more measured limits of this frontend (which decide why the code below looks like this)
#
# ① `break` / `next` **不生效**(实测 `while n < 5; break; end` 会一直转到 n = 5)—— **已修(v0.96.829)**,
# ① `break` / `next` **do not work** (measured: `while n < 5; break; end` runs all the way to n = 5),
# 所以每个循环都只靠**条件**退出(全文一条 `break` 都没有)。
# so every loop exits through its **condition** (there is not a single `break` in this file).
# ② `elsif` **不生效**(实测落进 `else` 分支)—— **已修(v0.96.829)**。多分支原来一律写成嵌套 `if`:
# ② `elsif` **does not work** (measured: it falls into the `else` branch) — multi-way branches are written as nested `if`s.
# ③ 浮点**打印**不通(`puts(3.5)` → 0)—— 本局只用整数,一个浮点都不出现。
# ③ Printing a **float** is broken (`puts(3.5)` -> 0) — this game is integer-only, not one float appears.
#
# ◆ 音效:**单音 `ui_audio_beep`**(本仓 §4.5 的规矩 —— 音序器在真机上破音,全部退回单音)
# ◆ Sound: **single-tone `ui_audio_beep`** (this repo's §4.5 rule — the sequencer crackles on real devices, everything went back to single tones)
#
# 打中外星人 1047(清亮)/ 掉命 262(低)/ 过关 1568(最高、最长)/ 结束 131(最低)。
# Hitting an invader 1047 (bright) / losing a life 262 (low) / level cleared 1568 (highest, longest) / game over 131 (lowest).
# 「赢取最高、输取最低」是 §4.5 定的规矩;低音不低于 C3(131Hz),再低手机外放就听不见了。
# "Highest for a win, lowest for a loss" is the §4.5 rule; no note below C3 (131 Hz), or a phone speaker will not reproduce it.
#
# ◆ 队形 / 炸弹用**自己写的伪随机**(LCG,与 `minesweeper.rb` 同一套算式)
# ◆ The formation and the bombs use a **home-made PRNG** (an LCG, the same recipe as `minesweeper.rb`)
#
# `rand` 走宿主 RNG(`SYSCALL #50`),那东西**没法从外面复现**;这里的 LCG 让"哪一颗炸弹"
# `rand` goes through the host RNG (`SYSCALL #50`), which **cannot be reproduced from outside**; this LCG makes "which bomb"
# 在一局里是确定的 —— 出问题时能重复同一局来定位。
# deterministic within a round — when something goes wrong the same round can be replayed to pin it down.
#
# 跑法:
# How to run:
# 手机 vml run examples/ruby/invaders.rb
# Phone vml run examples/ruby/invaders.rb
# 桌面 vmlcli Examples/ruby/invaders.rb --screen 395x578 --frames /tmp/fr
# Desktop vmlcli Examples/ruby/invaders.rb --screen 395x578 --frames /tmp/fr
# 喂输入 … --input 脚本(一行一条,如 `200 keydown 32` / `150 touchdown 120 400`)
# Feed input … --input script (one command per line, e.g. `200 keydown 32` / `150 touchdown 120 400`)
# UI 库的显式引用(用户 2026-10-01 定的口径,见 docs/UI库导入约定.md)。
# The explicit reference to the UI library (the user's convention of 2026-10-01; see the UI-library import convention doc under docs/).
# 声明文件 Lib/ruby/vml_ui.rb 由 GenLib 生成、不要手改:
# The declaration file Lib/ruby/vml_ui.rb is generated by GenLib; do not hand-edit it:
# dotnet run --project tools/GenLib -- -u -c vmlui -l ruby -o Lib/ruby/vml_ui.rb
# ⚠ 实测:本条是**文档级**引用 —— 删掉它示例照样编得过、跑得对(Ruby 按名字解析)。
# ⚠ Measured: this line is a **documentation-level** reference — delete it and the example still compiles and runs (Ruby resolves by name).
require 'vml_ui'
# ══════════════════════════════════════════════════════════════════════════
# 第一段:全部函数(⚠ 必须排在"给全局赋值"之前,理由见文件头那两条)
# Section 1: all the functions (⚠ they must come before any assignment to a global; see the two header notes)
# ══════════════════════════════════════════════════════════════════════════
# ⚠⚠ 编译期保险,不是游戏逻辑,**不需要被调用**(调用它也无害,只会读到 0)。
# ⚠⚠ A compile-time safeguard, not game logic; it **does not need to be called** (calling it is harmless and just reads zeros).
# 把"别的函数要写的状态名"读一遍 ⇒ 它们的全局槽在任何函数生成之前就已存在(见文件头第二半)。
# It reads every state name the other functions write ⇒ their global slots exist before any of those functions is generated (see the second header note).
# ⚠ 名单要**跟着新状态一起加**:漏一个的后果是"那个变量在函数里改了、外面看不到",而且不报错。
# ⚠ Keep this list **in step with every new piece of state**: leave one out and "the function changes it but nobody outside sees it", with no error at all.
def iv_touch_state()
return g_score + g_best + g_lives + g_level + g_phase + g_hold + g_tick + g_dir + g_fx + g_fy + g_n + g_move_every + g_bullet_on + g_bullet_x + g_bullet_y + g_bomb_every + g_bomb_cool + g_seed + g_games + g_px + g_py + g_alive
end
# ── 自己写的伪随机:LCG(乘 75 加 74、模 65537)。参数是**量出来的**:
# ── The home-made PRNG: an LCG (times 75 plus 74, mod 65537). The parameters were **measured**:
# 常见的"乘 1103 加 12345、模 32768"取模 80 之后只有 10 个不同的值(低位周期极短)⇒ 挑格子会挑不动。
# the common "times 1103 plus 12345, mod 32768" yields only 10 distinct values after a modulo 80 (the low bits have a very short period) ⇒ picking cells stalls.
def iv_next_seed()
g_seed = (g_seed * 75 + 74) % 65537
return g_seed
end
# ── 一个外星人(第 i 个)的左上角坐标:i 的行列由 i / cols、i % cols 推 ──
# ── The top-left corner of invader i: its row and column come from i / cols and i % cols ──
def iv_inv_x(p_i)
p_c = p_i % g_cols
return g_fx + p_c * g_cellw + (g_cellw - g_iw) / 2
end
def iv_inv_y(p_i)
p_r = p_i / g_cols
return g_fy + p_r * g_cellh + (g_cellh - g_ih) / 2
end
# ── 开一炮(同时在飞的只有一发;这是老式街机的规矩,也让"什么时候开炮"成为决策)──
# ── Fire a shot (only one is in flight at a time; that is the old arcade rule, and it makes "when to shoot" a decision) ──
def iv_fire()
if g_phase != 0
return 0
end
if g_bullet_on == 0
g_bullet_on = 1
g_bullet_x = g_px
g_bullet_y = g_py - 10
end
return 0
end
# ── 摆一关的队形:外星人全满、队形回到左上、速度按关卡加快 ──
# ── Lay out one level: every invader alive, the formation back at the top left, speed up with the level ──
def iv_spawn_level()
i = 0
while i < g_n_max
g_inv[i] = 1
i = i + 1
end
g_n = g_n_max
g_fx = g_margin
g_fy = g_top
g_dir = 1
g_bullet_on = 0
g_bombs.clear
g_bomb_cool = 20
# 每关快一点:9 拍挪一次,到第 6 关封顶在 4 拍
# A bit faster each level: one move every 9 ticks, floored at 4 ticks by level 6
g_move_every = 9 - g_level
if g_move_every < 4
g_move_every = 4
end
return 0
end
# ── 开新局 ──
# ── Start a new round ──
def iv_new_game()
g_score = 0
g_lives = 5
g_level = 1
g_phase = 0
g_tick = 0
g_px = g_w / 2
g_py = g_h - 46
g_hold = 0
g_games = g_games + 1
g_seed = 10068 + g_games * 7919
iv_spawn_level()
return 0
end
# ── 结束这一局:把分数记进成绩表(数组 push / max / shift 都在这儿用上)──
# ── End the round: record the score in the score table (this is where push / max / shift of the array library earn their keep) ──
def iv_game_over()
g_phase = 2
g_scores.push(g_score)
if g_scores.length > 5
g_scores.shift
end
b = g_scores.max
if b > g_best
g_best = b
end
# 输:取最低音(§4.5 的规矩)
# Lose: the lowest note (the §4.5 rule)
ui_audio_beep(131, 320)
return 0
end
# ── 过关:加分、进下一关 ──
# ── Level cleared: award the bonus and move on ──
def iv_level_clear()
g_score = g_score + 100 + g_level * 20
g_level = g_level + 1
g_phase = 1
g_hold = 14
iv_spawn_level()
# 赢:取最高音、最长(§4.5 的规矩)
# Win: the highest and longest note (the §4.5 rule)
ui_audio_beep(1568, 320)
return 0
end
# ── 一拍物理:外星人整队移动 / 子弹上飞 / 炸弹下落 / 各类碰撞 ──
# ── One physics tick: the formation moves / the bullet flies up / the bombs fall / all the collisions ──
def iv_tick_step()
# 过关后停 14 拍(约 0.6 秒)展示横幅 —— 玩家那一刻正盯着自己的飞船,需要这半秒
# After a level is cleared, freeze for 14 ticks (about 0.6 s) to show the banner — the player is looking at their own ship at that moment and needs that half second
if g_phase == 1
g_hold = g_hold - 1
if g_hold <= 0
g_phase = 0
end
return 0
end
if g_phase != 0
return 0
end
g_tick = g_tick + 1
# ── 外星人:每 move_every 拍整队横移一格;碰到左右墙就整队下沉并掉头 ──
# ── Invaders: the whole formation shifts one step every move_every ticks; hitting a wall drops it and reverses direction ──
if g_tick % g_move_every == 0
nx = g_fx + g_dir * g_step
if nx + g_cols * g_cellw > g_w - g_margin
g_dir = 0 - 1
g_fy = g_fy + g_drop
else
if nx < g_margin
g_dir = 1
g_fy = g_fy + g_drop
else
g_fx = nx
end
end
end
# ── 子弹上飞 + 打中外星人 ──
# ── The bullet flies up and may hit an invader ──
if g_bullet_on == 1
g_bullet_y = g_bullet_y - g_bspeed
if g_bullet_y < g_top - 20
g_bullet_on = 0
else
i = 0
while i < g_n_max
if g_inv[i] == 1
ix = iv_inv_x(i)
iy = iv_inv_y(i)
if g_bullet_x >= ix && g_bullet_x <= ix + g_iw && g_bullet_y <= iy + g_ih && g_bullet_y >= iy
g_inv[i] = 0
g_n = g_n - 1
g_bullet_on = 0
g_score = g_score + 10 * g_level
ui_audio_beep(1047, 90)
end
end
i = i + 1
end
end
end
# ── 自动补炮:只要上一发不在飞就再发一发 —— 手机上"一根手指只管走位",这也是移动端射击游戏的通行做法 ──
# ── Auto-fire: as soon as nothing is in flight, shoot again — on a phone "one finger only steers", which is how mobile shooters normally play ──
if g_bullet_on == 0
iv_fire()
end
# ── 炸弹:每 bomb_every 拍从随机一个活外星人那里丢一颗 ──
# ── Bombs: every bomb_every ticks one is dropped from a randomly chosen live invader ──
g_bomb_cool = g_bomb_cool - 1
if g_bomb_cool <= 0
g_bomb_cool = g_bomb_every
if g_bombs.length < 3
if g_n > 0
k = 0
done = 0
while k < 12
if done == 0
iv_next_seed()
idx = g_seed % g_n_max
if g_inv[idx] == 1
done = 1
bx = iv_inv_x(idx) + g_iw / 2
by = iv_inv_y(idx) + g_ih
g_bombs.push(by * 1024 + bx)
end
end
k = k + 1
end
end
end
end
# ── 炸弹下落:整表过一遍,出屏的丢掉、砸到飞船的扣命(新表用 push 攒出来)──
# ── Bombs fall: walk the table, drop the off-screen ones, charge a life for a hit (the survivors are collected into a new table with push) ──
kept = []
i = 0
while i < g_bombs.length
e = g_bombs[i]
bx = e % 1024
by = e / 1024 + g_bombspeed
if by > g_h
i = i + 1
else
hit = 0
if by > g_py - 12 && by < g_py + 20
if bx > g_px - 26 && bx < g_px + 26
hit = 1
end
end
if hit == 1
g_lives = g_lives - 1
ui_audio_beep(262, 200)
if g_lives <= 0
iv_game_over()
end
else
kept.push(by * 1024 + bx)
end
i = i + 1
end
end
g_bombs = kept
# ── 外星人压到飞船那一行 = 结束 ──
# ── Invaders reaching the ship's row ends the round ──
if g_phase == 0
if g_fy + (g_rows - 1) * g_cellh + g_ih >= g_py - 6
iv_game_over()
end
end
# ── 全清 = 过关 ──
# ── All dead = level cleared ──
if g_phase == 0
if g_n <= 0
iv_level_clear()
end
end
return 0
end
# ── 点一处:飞船直接挪到点的地方(手机上是绝对定位,跳一步比慢慢挪好按)──
# ── A tap: the ship jumps straight to the tapped position (absolute positioning on a phone: a jump beats sliding slowly) ──
def iv_tap(p_x, p_y)
if g_phase != 0
return 0
end
# 点哪儿就往哪儿靠(手机上是绝对定位,跳一步比慢慢挪好按)
# Move towards wherever the tap landed (on a phone this is absolute positioning: a jump beats sliding slowly)
g_px = p_x
if g_px < 26
g_px = 26
end
if g_px > g_w - 26
g_px = g_w - 26
end
iv_fire()
return 0
end
# ── 画一帧 ──
# ── Draw one frame ──
def iv_draw()
ui_2d_clear(g_c_bg)
# 顶栏:分数 / 关卡 / 命(命画成小飞船,数字转字符串这条路能用,但画图更直观)
# Top bar: score / level / lives (lives are drawn as little ships — number-to-string works, but the picture reads faster)
# ⚠ 中文字面量与语言条件要写在**同一行** —— 本仓 `scripts/examples-lang-audit.py` 就是这么判的
# ⚠ The Chinese literal and the language condition must sit on the **same line** — that is how `scripts/examples-lang-audit.py` checks it
# (多行 if/else 更易读,但过不了那条判据;同目录的 catch.rb / demo_ui.rb 也都是同一行的写法)
# (a multi-line if/else reads better but fails that check; catch.rb / demo_ui.rb in this same directory write it on one line too)
if g_lang == 0 then ui_2d_text(10, 8, "得分 " + g_score.to_s, g_c_text, 14, VML_ANCHOR_LEFT) else ui_2d_text(10, 8, "Score " + g_score.to_s, g_c_text, 14, VML_ANCHOR_LEFT) end
if g_lang == 0 then ui_2d_text(g_w / 2 + 20, 8, "第 " + g_level.to_s + " 关", g_c_text, 14, VML_ANCHOR_LEFT) else ui_2d_text(g_w / 2 + 20, 8, "Level " + g_level.to_s, g_c_text, 14, VML_ANCHOR_LEFT) end
if g_lang == 0 then ui_2d_text(g_w - 10, 8, "最高 " + g_best.to_s, g_c_dim, 12, VML_ANCHOR_RIGHT) else ui_2d_text(g_w - 10, 8, "Best " + g_best.to_s, g_c_dim, 12, VML_ANCHOR_RIGHT) end
i = 0
while i < g_lives
ui_2d_rect(g_w - 14 - i * 18, 26, 12, 8, g_c_ship, 1, 0, 2)
i = i + 1
end
# 外星人:三行三种颜色(一眼能看出还剩几行)
# Invaders: three rows in three colours (how many rows are left is visible at a glance)
i = 0
while i < g_n_max
if g_inv[i] == 1
r = i / g_cols
c = g_c_i1
if r == 1
c = g_c_i2
end
if r == 2
c = g_c_i3
end
x = iv_inv_x(i)
y = iv_inv_y(i)
ui_2d_rect(x, y, g_iw, g_ih, c, 1, 0, 4)
ui_2d_rect(x + 4, y + 5, 5, 5, g_c_bg, 1, 0, 1)
ui_2d_rect(x + g_iw - 9, y + 5, 5, 5, g_c_bg, 1, 0, 1)
end
i = i + 1
end
# 炸弹
# The bombs
i = 0
while i < g_bombs.length
e = g_bombs[i]
bx = e % 1024
by = e / 1024
ui_2d_rect(bx - 4, by, 8, 10, g_c_bomb, 1, 0, 2)
i = i + 1
end
# 子弹
# The bullet
if g_bullet_on == 1
ui_2d_rect(g_bullet_x - 2, g_bullet_y, 4, 12, g_c_bullet, 1, 0, 2)
end
# 飞船:一条船身 + 中间那根炮管
# The ship: a hull plus the cannon in the middle
ui_2d_rect(g_px - 23, g_py, 46, 12, g_c_ship, 1, 0, 4)
ui_2d_rect(g_px - 3, g_py - 8, 6, 10, g_c_ship, 1, 0, 2)
# 结局横幅
# The end-of-round banner
if g_phase == 1
ui_2d_rect(g_pad, g_h / 2 - 30, g_w - g_pad * 2, 60, g_c_win, 1, 0, 10)
if g_lang == 0 then ui_2d_text(g_w / 2, g_h / 2 - 14, "过关!按 R 重新开始", g_c_bg, 16, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_h / 2 - 14, "Cleared! Press R", g_c_bg, 16, VML_ANCHOR_CENTER) end
end
if g_phase == 2
ui_2d_rect(g_pad, g_h / 2 - 30, g_w - g_pad * 2, 60, g_c_lose, 1, 0, 10)
if g_lang == 0 then ui_2d_text(g_w / 2, g_h / 2 - 14, "结束,按 R 再来一局", g_c_bg, 16, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_h / 2 - 14, "Game over, press R", g_c_bg, 16, VML_ANCHOR_CENTER) end
if g_lang == 0 then ui_2d_text(g_w / 2, g_h / 2 + 8, "本局得分 " + g_score.to_s, g_c_bg, 13, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_h / 2 + 8, "Score " + g_score.to_s, g_c_bg, 13, VML_ANCHOR_CENTER) end
# 历史前三(成绩表用 `sort` + `reverse` + `take` 排出来 —— 这三个方法在这一版才修好)
# The top three so far (the score table is ranked with `sort` + `reverse` + `take` — all three were fixed in this round)
rk = g_scores.sort
rk = rk.reverse
top = rk.take(3)
txt = ""
k = 0
while k < top.length
txt = txt + top[k].to_s + " "
k = k + 1
end
if g_lang == 0 then ui_2d_text(g_w / 2, g_h / 2 + 30, "历史前三 " + txt, g_c_bg, 12, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_h / 2 + 30, "Top three " + txt, g_c_bg, 12, VML_ANCHOR_CENTER) end
end
# 底部操作提示(手机上是点、桌面是键)
# The hint line at the bottom (tap on a phone, keys on the desktop)
if g_lang == 0 then ui_2d_text(g_w / 2, g_h - 22, "点哪儿飞船去哪儿(自动开炮)· R 重开 · Esc 退出", g_c_dim, 12, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_h - 22, "Tap to steer (auto-fire) · R restart · Esc quit", g_c_dim, 12, VML_ANCHOR_CENTER) end
return 0
end
# ══════════════════════════════════════════════════════════════════════════
# 第二段:布局与状态(⚠ 全部写在 `def` 之后 —— 见文件头那两条)
# Section 2: layout and state (⚠ all of it after the `def`s — see the two header notes)
# ══════════════════════════════════════════════════════════════════════════
# ── 配色(负数十进制 = 0xAARRGGBB)──
# ── Palette (negative decimals = 0xAARRGGBB) ──
g_c_bg = -16119276 # 0xFF0A0A14
g_c_panel = -15066588 # 0xFF1A1A24
g_c_text = -1513232 # 0xFFE8E8F0
g_c_dim = -6643536 # 0xFF9AA0B0
g_c_i1 = -8592501 # 0xFF7CE38B row 0
g_c_i2 = -9517313 # 0xFF6EC6FF row 1
g_c_i3 = -2056112 # 0xFFE0A050 row 2
g_c_ship = -11409298 # 0xFF51E86E ship
g_c_bullet = -8096 # 0xFFFFE060 bullet
g_c_bomb = -2075821 # 0xFFE05353 bomb
g_c_win = -11483016 # 0xFF50C878 level-cleared banner
g_c_lose = -2075821 # 0xFFE05353 game-over banner
# ── 布局:**开窗时传 0(尺寸交给宿主),开窗之后再问真实画布**,位置全部由 w / h 算出来 ──
# ── Layout: **open with 0 (the host decides the size), ask the real canvas after the window is open**; every position is computed from w / h ──
g_lang = ui_scr_get_language()
if g_lang == 0 then ui_win_open("太空侵略者", 0, 0) else ui_win_open("Space Invaders", 0, 0) end
ui_scr_set_keep_on(1)
# **开窗之后**再问一次真实画布 —— 开窗前问到的只是估算值
# Ask the **real** canvas only after the window is open -- before that the host can only estimate
g_w = ui_scr_get_w()
if g_w <= 0
g_w = 395
end
g_h = ui_scr_get_h()
if g_h <= 0
g_h = 578
end
# 队形几何:格子宽由画布宽推出来(留出左右各 2 格的行走余地,队形才动得起来)
# Formation geometry: the cell width is derived from the canvas width (leaving two cells of travel on each side, so the formation can actually move)
g_rows = 3
g_cols = 5
g_pad = 14
g_top = 56
g_margin = 12
g_cellw = (g_w - g_margin * 2) / (g_cols + 2)
g_cellh = g_cellw * 3 / 4
g_iw = g_cellw - 12
g_ih = g_cellh - 12
g_n_max = g_rows * g_cols
# 速度:像素 / 拍(一拍 = 40ms)。整数物理,没有浮点。
# Speeds in pixels per tick (one tick = 40 ms). Integer physics, no floats.
g_step = 6
g_drop = 14
g_bspeed = 14
g_bombspeed = 4
g_bomb_every = 30
# ── 状态 ──
# ── State ──
g_inv = []
g_i = 0
while g_i < g_n_max
g_inv.push(1)
g_i = g_i + 1
end
g_bombs = []
g_scores = []
g_score = 0
g_best = 0
g_lives = 5
g_level = 1
g_phase = 0
g_tick = 0
g_dir = 1
g_fx = g_margin
g_fy = g_top
g_n = g_n_max
g_move_every = 8
g_bullet_on = 0
g_bullet_x = 0
g_bullet_y = 0
g_bomb_cool = 20
g_seed = 10068
g_games = 0
g_px = g_w / 2
g_py = g_h - 46
g_hold = 0
g_alive = 1
# ── 开局:定时器每 40ms 一拍(物理节拍),消息循环里把输入与拍子分开处理 ──
# ── Start: a timer ticks every 40 ms (the physics beat); the message loop handles input and beats separately ──
iv_new_game()
g_tickms = 40 # 物理一拍 = 40ms(墙钟口径,见主循环里的补步)
# one physics beat = 40 ms (wall-clock basis; see the catch-up in the main loop)
g_tid = ui_timer_set(g_tickms, 0)
# 墙钟补步累加器(按墙钟补步,用户 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)
g_acc = 0
g_lastms = ui_time_get_ms() # 墙钟起点(进主循环时取)
# wall-clock origin, sampled when entering the main loop
while g_alive == 1
iv_draw()
ui_2d_present()
# 按墙钟补步,用户 2026-10-04 口径:游戏时钟取系统毫秒数。
# steps are caught up from the wall clock; user's 2026-10-04 rule: all game clocks read system milliseconds.
# 定时器只做唤醒:欠账时不等消息、立刻补跑一拍;欠账还清才回去等。
# the timer only wakes us up: while in deficit it does not wait for a message and runs one beat immediately; it goes back to waiting once the deficit is cleared.
now2 = ui_time_get_ms()
g_acc = g_acc + (now2 - g_lastms)
g_lastms = now2
if g_acc > g_tickms * 8
g_acc = 0 # 卡太久别一次补一大串
# do not catch up in one huge burst after a long stall
end
t = 0
if g_acc < g_tickms
t = ui_msg_fetch_wait(0)
end
if g_acc >= g_tickms
g_acc = g_acc - g_tickms
iv_tick_step()
end
if t == 1
k = ui_msg_get_a()
if k == 27
g_alive = 0
end
if k == 37
g_px = g_px - 22
end
if k == 39
g_px = g_px + 22
end
if k == 32 || k == 38 || k == 13
iv_fire()
end
# R = 随时重开(与扫雷同一套手感:不用先死一次才能重来)
# R = restart at any time (the same feel as minesweeper: no need to die first before starting over)
if k == 82 || k == 114
iv_new_game()
end
if g_px < 26
g_px = 26
end
if g_px > g_w - 26
g_px = g_w - 26
end
end
if t == 6
iv_tap(ui_msg_get_a(), ui_msg_get_b())
end
if t == 7
# 拖动会灌进来大量 TOUCHMOVE(本仓记过:队列会涨到几千条)⇒ 拖动的处理是"只跟手不重复开炮"
# Dragging floods the queue with TOUCHMOVE messages (this repo has recorded thousands of them) ⇒ dragging just steers, it does not fire again
x = ui_msg_get_a()
if g_phase == 0
g_px = x
if g_px < 26
g_px = 26
end
if g_px > g_w - 26
g_px = g_w - 26
end
end
ui_msg_drop(1)
end
if t == 10
g_alive = 0
end
if ui_win_is_closed() != 0
g_alive = 0
end
end
ui_timer_kill(g_tid)
ui_scr_set_keep_on(0)
ui_win_close()
# 给无头验证留几行**确定性**的判据(画面只能肉眼看,这几个数能自动比)
# A few **deterministic** checks are left for headless verification (the picture can only be eyeballed; these numbers can be compared automatically)
print("invaders: canvas=")
print(g_w)
print("x")
print(g_h)
print(" cols=")
print(g_cols)
print(" rows=")
print(g_rows)
print("\n")
print("invaders: score=")
print(g_score)
print(" level=")
print(g_level)
print(" lives=")
print(g_lives)
print(" alive=")
print(g_n)
print(" phase=")
print(g_phase)
print(" best=")
print(g_best)
print(" games=")
print(g_games)
print("\n")
print("invaders: done\n")