← 都来码 / Dolaima← 例程索引ruby/app/invaders.rb原文

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")