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

ruby/app/minesweeper.rb · 31113 字节 · 下载原文

# 扫雷 —— 用 **Ruby** 写的手机小游戏(回合制 / 逻辑推演)

# Minesweeper — a small phone game written in **Ruby** (turn based / logic)

#

# 玩法:点开格子,数字表示它周围 8 格里有几颗雷;把**所有不是雷的格子**翻开就通关。

# How to play: tap a cell to open it; the number tells how many of the 8 neighbours hold a mine; open **every cell that is not a mine** to win.

# 踩到雷就这一局结束。旗标模式(屏幕下方的按钮 / 空格键)用来给格子插旗标记。

# Tapping a mine ends the round. The flag mode (the button below the board / the space key) plants a flag on a cell.

#

# ◆ 操作

# ◆ Controls

#

#   触摸:点格子 = 翻开(旗标模式下 = 插旗);点下方按钮切模式 / 重开。

#   Touch: tap a cell = open (or flag, in flag mode); tap the buttons below to switch mode / restart.

#   键盘:方向键移光标、回车翻开、空格切旗标模式、F 插旗、R 重开、Esc 退出。

#   Keyboard: arrows move the cursor, Enter opens, Space toggles flag mode, F flags, R restarts, Esc quits.

#

# ◆⚠ 本前端最坑的一条(本文件实测 2026-10-01):**函数的写法决定"全局"能不能被看见**

# ◆⚠ The nastiest item on this frontend (measured on this file, 2026-10-01): **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**; the layout and state (the `g_`-prefixed batch) come second.

#

#   最小复现(三份探针,都是一行之差):

#   Minimal reproductions (three probes, each one line apart):

#

#     g_a = 111                     # ① 赋值在 def 之前

#     g_a = 111                     # ① assigned before the def

#     def f(); return g_a; end

#     def f(); return g_a; end

#     print(f())                    # → 61     ✗(期望 111;那个局部是垃圾)

#     print(f())                    # -> 61     ✗ (111 expected; that local is garbage)

#

#     def f(); return g_a; end      # ② 赋值在 def 之后

#     def f(); return g_a; end      # ② assigned after the def

#     g_a = 222

#     g_a = 222

#     print(f())                    # → 222    ✔

#     print(f())                    # -> 222    ✔

#

#     g_a = 111; def f(); return g_a; end; g_a = 333; print(f())    # → 61   ✗(后面再赋值也救不回来)

#     g_a = 111; def f(); return g_a; end; g_a = 333; print(f())    # -> 61   ✗ (a later assignment does not save it)

#

#   ⚠ 这一条以前没被任何例程覆盖到:`catch.rb` / `demo_ui.rb` 都是**完全平铺**的(一个 `def` 都没有),

#   ⚠ No earlier example covered this: `catch.rb` / `demo_ui.rb` are **completely flat** (not one `def`),

#   而 `stringutil/main.rb` 的规模又太小、恰好没踩上。⇒ 写带函数的例程时**先摆 def、再赋全局**。

#   and `stringutil/main.rb` is too small to have hit it. ⇒ When writing an example with functions, **define first, assign globals second**.

#

# ◆ 本前端另外三条实测限制(决定了下面为什么这么写)

# ◆ 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.

#

# ◆ 雷区是**自己写的伪随机**(不用 `rand`)

# ◆ The mine field uses a **home-made PRNG** (not `rand`)

#

#   `srand/rand` 走宿主 RNG(`SYSCALL #50/51`),同一份代码两次跑出来的雷区不一样,

#   `srand/rand` goes through the host RNG (`SYSCALL #50/51`), so the same code produces a different field on every run,

#   无头验收就无从写"点到哪一格"。这里用一个 3 行的 LCG(乘 75 加 74、模 65537),

#   which leaves headless acceptance with no way to say "tap this cell". Here we use a 3-line LCG (times 75 plus 74, mod 65537),

#   算式与验收脚本(Python)里复刻的那份逐字相同 ⇒ 脚本能**先算出雷区**再喂坐标。

#   whose formula is byte-for-byte the same as the copy reproduced in the acceptance script (Python) ⇒ the script can **compute the field beforehand** and then feed coordinates.

#   ⚠ 参数是**量出来的**,不是随手挑的:`乘 1103 加 12345、模 32768`(常见的 15 位写法)

#   ⚠ The parameters were **measured**, not picked at random: `times 1103 plus 12345, mod 32768` (the common 15-bit recipe)

#   取模 80 之后**只有 10 个不同的值**(LCG 的低位周期极短)⇒ 布雷循环永远凑不齐 12 颗、

#   yields **only 10 distinct values** after a modulo 80 (the low bits of an LCG have a very short period) ⇒ the placing loop never reaches 12 mines

#   直接**死循环**(Python 复刻时实测:转满 2000 步仍只有那 10 个)。换成模 65537(质数)+ 乘 75,

#   and just **hangs** (measured while reproducing it in Python: 2000 steps still gave only those 10). With the prime modulus 65537 and multiplier 75

#   取模 80 覆盖满 80 个值,第一局 13 步就布完 12 颗。

#   the modulo-80 residues cover all 80 values, and the first round places its 12 mines in 13 steps.

#

# 跑法:

# How to run:

#   手机    vml run examples/ruby/minesweeper.rb

#   Phone   vml run examples/ruby/minesweeper.rb

#   桌面    vmlcli Examples/ruby/minesweeper.rb --screen 395x578 --frames /tmp/fr

#   Desktop vmlcli Examples/ruby/minesweeper.rb --screen 395x578 --frames /tmp/fr

#   喂输入  … --input 脚本(一行一条,如 `200 touchdown 120 200` / `150 keydown 13`)

#   Feed input  … --input script (one command per line, e.g. `200 touchdown 120 200` / `150 keydown 13`)


# 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 header note)

# ══════════════════════════════════════════════════════════════════════════


# ⚠⚠ 这个函数是**编译期保险**,不是游戏逻辑,**不需要被调用**(调用它也无害,只会读到 0)。

# ⚠⚠ This function is a **compile-time safeguard**, not game logic; it **does not need to be called** (calling it is harmless and just reads zeros).

#

# 真身(2026-10-01 在 `RubyCompiler/CodeGenerator.Expressions.cs` 的 `GenerateVar` +

# The real story (measured 2026-10-01 in `GenerateVar` of `RubyCompiler/CodeGenerator.Expressions.cs` plus

# `CompilerBase/CodeGeneratorBase.cs` 的 `EmitStoreVar` 里量出来的):

# `EmitStoreVar` of `CompilerBase/CodeGeneratorBase.cs`):

#

#   · 顶层变量与函数局部是**两张表**。一个名字只有在**被函数体读过**之后,才会生成

#   · Top-level variables and function locals live in **two tables**. Only after a name is **read inside a function body**

#     全局数据槽 `var_<名>`;`EmitStoreVar` 的顺序是:局部表命中 → 全局槽已存在 → **否则新开一个局部**。

#     does the global data slot `var_<name>` come into being; `EmitStoreVar` goes: local table hit -> global slot exists -> **otherwise allocate a fresh local**.

#   · ⇒ **"只写不读"的名字,在函数里写的是该函数自己的局部**(顶层那个纹丝不动,而且不报错)。

#   · ⇒ **A name that is only written and never read gets written to that function's own local** (the top-level one never changes, and nothing is reported).

#     实测:`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.

#

# ⇒ 下面这个函数把"各函数要写的状态名"**先读一遍**,保证它们的全局槽在任何函数生成之前就存在。

# ⇒ The function below **reads once** every state name that the other functions write, so their global slots exist before any function is generated.

def ms_touch_state()
  return g_seed + g_game + g_opened + g_flags + g_phase + g_first + g_cur_r + g_cur_c + g_flagmode + g_alive
end

# ── 数字 1..8 的颜色 ──

# ── The colour of the digits 1..8 ──

def ms_num_color(p_n)
  if p_n == 1
    return -11890471       # 0xFF4A90D9

  end
  if p_n == 2
    return -12606643       # 0xFF3FA34D

  end
  if p_n == 3
    return -2075821        # 0xFFE05353

  end
  if p_n == 4
    return -13877617       # 0xFF2C3E8F

  end
  if p_n == 5
    return -7655890        # 0xFF8B2E2E

  end
  if p_n == 6
    return -13726837       # 0xFF2E8B8B

  end
  if p_n == 7
    return -1513232        # 0xFFE8E8F0

  end
  return -6643536          # 0xFF9AA0B0

end

# ── 数一个格子周围 8 格里有几颗雷 ──

# ── Count how many of the 8 neighbours of a cell hold a mine ──

def ms_count_adj(p_idx)
  p_r = p_idx / g_cols
  p_c = p_idx % g_cols
  n = 0
  dr = 0 - 1
  while dr < 2
    dc = 0 - 1
    while dc < 2
      rr = p_r + dr
      cc = p_c + dc
      if (dr != 0 || dc != 0) && rr >= 0 && rr < g_rows && cc >= 0 && cc < g_cols
        if g_mine[rr * g_cols + cc] == 1
          n = n + 1
        end
      end
      dc = dc + 1
    end
    dr = dr + 1
  end
  return n
end

# ── 开新局:清空三个数组,把随机种子按局数往前推(每局雷区不同)──

# ── Start a new round: clear the three arrays and advance the seed by the round number (a different field each round) ──

def ms_new_game()
  i = 0
  while i < g_n
    g_mine[i] = 0
    g_state[i] = 0
    g_adj[i] = 0
    i = i + 1
  end
  g_opened = 0
  g_flags = 0
  g_phase = 0
  g_first = 0
  g_cur_r = g_rows / 2
  g_cur_c = g_cols / 2
  g_game = g_game + 1
  # 种子 = 10068(= 20261001 mod 65537)+ 局数 * 7919 ⇒ 每局雷区不同,且 Python 那边能逐字复刻

  # Seed = 10068 (= 20261001 mod 65537) + round * 7919 ⇒ a different field every round, byte-for-byte reproducible in Python

  g_seed = 10068 + g_game * 7919
  return 0
end

# ── 布雷:第一下点的那一格(p_avoid)一定不放雷 ──

# ── Place the mines: the cell of the first tap (p_avoid) never gets one ──

def ms_place_mines(p_avoid)
  placed = 0
  while placed < g_mines_total
    g_seed = (g_seed * 75 + 74) % 65537
    idx = g_seed % g_n
    if idx != p_avoid
      if g_mine[idx] == 0
        g_mine[idx] = 1
        placed = placed + 1
      end
    end
  end
  i = 0
  while i < g_n
    if g_mine[i] == 0
      g_adj[i] = ms_count_adj(i)
    else
      g_adj[i] = 0
    end
    i = i + 1
  end
  return 0
end

# ── 翻开一格:周围 0 颗雷的格子往外**泛洪**(用数组当栈 —— 递归也行,但栈更稳)──

# ── Open a cell: a cell with 0 neighbouring mines **floods** outwards (recursion works too, but a stack is steadier) ──

def ms_open(p_idx)
  if g_phase != 0
    return 0
  end
  if g_state[p_idx] != 0
    return 0
  end
  if g_first == 0
    g_first = 1
    ms_place_mines(p_idx)
  end
  if g_mine[p_idx] == 1
    g_state[p_idx] = 3
    g_phase = 2
    # 输:取最低音(本仓 §4.5 的规矩 —— 赢取最高、输取最低,不看屏幕也分得出)

    # Lose: the lowest note (this repo's §4.5 rule — highest for a win, lowest for a loss, tellable without looking)

    ui_audio_beep(131, 320)
    return 0
  end
  stack = []
  stack.push(p_idx)
  while stack.length > 0
    cur = stack.pop
    if g_state[cur] == 0
      g_state[cur] = 1
      g_opened = g_opened + 1
      if g_adj[cur] == 0
        cur_r = cur / g_cols
        cur_c = cur % g_cols
        dr = 0 - 1
        while dr < 2
          dc = 0 - 1
          while dc < 2
            rr = cur_r + dr
            cc = cur_c + dc
            if (dr != 0 || dc != 0) && rr >= 0 && rr < g_rows && cc >= 0 && cc < g_cols
              n2 = rr * g_cols + cc
              if g_state[n2] == 0 && g_mine[n2] == 0
                stack.push(n2)
              end
            end
            dc = dc + 1
          end
          dr = dr + 1
        end
      end
    end
  end
  if g_opened >= g_n - g_mines_total
    g_phase = 1
    # 赢:取最高音、最长(同上那条规矩)

    # Win: the highest and longest note (the same rule as above)

    ui_audio_beep(1568, 320)
  else
    # 普通一次翻开:清亮的一点提示音

    # An ordinary open: one bright blip

    ui_audio_beep(880, 60)
  end
  return 0
end

# ── 插旗 / 撤旗 ──

# ── Plant or take back a flag ──

def ms_flag(p_idx)
  if g_phase != 0
    return 0
  end
  if g_state[p_idx] == 0
    g_state[p_idx] = 2
    g_flags = g_flags + 1
    ui_audio_beep(1047, 60)
  else
    if g_state[p_idx] == 2
      g_state[p_idx] = 0
      g_flags = g_flags - 1
      ui_audio_beep(660, 60)
    end
  end
  return 0
end

# ── 点一处:先看是不是按在两个按钮上,再当格子处理 ──

# ── A tap: first check whether one of the two buttons was hit, then treat it as a cell ──

def ms_tap(p_x, p_y)
  if p_y >= g_btn_y && p_y <= g_btn_y + g_btn_h
    if p_x >= g_btn1_x && p_x <= g_btn1_x + g_btn_w
      if g_flagmode == 0
        g_flagmode = 1
      else
        g_flagmode = 0
      end
      ui_audio_beep(784, 60)
      return 0
    end
    if p_x >= g_btn2_x && p_x <= g_btn2_x + g_btn_w
      ms_new_game()
      return 0
    end
  end
  if p_x < g_bx || p_y < g_by
    return 0
  end
  p_c = (p_x - g_bx) / g_cell
  p_r = (p_y - g_by) / g_cell
  if p_c < 0 || p_c >= g_cols
    return 0
  end
  if p_r < 0 || p_r >= g_rows
    return 0
  end
  g_cur_r = p_r
  g_cur_c = p_c
  idx = p_r * g_cols + p_c
  if g_flagmode == 1
    ms_flag(idx)
  else
    ms_open(idx)
  end
  return 0
end

# ── 画一帧 ──

# ── Draw one frame ──

def ms_draw()
  ui_2d_clear(g_c_bg)
  # ⚠ 中文字面量与语言条件要写在**同一行** —— 本仓 `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(g_w / 2, 10, "扫雷 Minesweeper", g_c_title, 16, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, 10, "Minesweeper 扫雷", g_c_title, 16, VML_ANCHOR_CENTER) end
  if g_lang == 0 then ui_2d_text(g_w / 2, 32, "回车翻开 / 空格切旗标 / R 重开", g_c_dim, 12, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, 32, "Enter opens / Space toggles flag / R restarts", g_c_dim, 12, VML_ANCHOR_CENTER) end
  # 顶栏右侧:剩余雷数(总雷数 − 已插旗数)

  # Right side of the top bar: mines remaining (total mines minus flags planted)

  ui_2d_rect(g_w - 96, 8, 88, 20, g_c_panel, 1, 0, 5)
  if g_lang == 0 then ui_2d_text(g_w - 52, 11, "剩 " + (g_mines_total - g_flags).to_s, g_c_flag, 13, VML_ANCHOR_CENTER) else ui_2d_text(g_w - 52, 11, "left " + (g_mines_total - g_flags).to_s, g_c_flag, 13, VML_ANCHOR_CENTER) end
  # 网格:三种状态各画各的(没翻开 / 插旗 / 翻开)

  # The grid: each of the three states is drawn its own way (closed / flagged / opened)

  i = 0
  while i < g_n
    r = i / g_cols
    c = i % g_cols
    x = g_bx + c * g_cell
    y = g_by + r * g_cell
    s = g_state[i]
    if s == 0
      ui_2d_rect(x + 1, y + 1, g_cell - 2, g_cell - 2, g_c_hidden, 1, 0, 3)
    end
    if s == 2
      ui_2d_rect(x + 1, y + 1, g_cell - 2, g_cell - 2, g_c_panel, 1, 0, 3)
      ui_2d_rect(x + g_cell / 2 - 1, y + 8, 3, g_cell - 14, g_c_flag, 1, 0, 1)
      ui_2d_rect(x + g_cell / 2 + 1, y + 9, g_cell / 3, g_cell / 3, g_c_flag, 1, 0, 1)
    end
    if s == 1
      ui_2d_rect(x + 1, y + 1, g_cell - 2, g_cell - 2, g_c_open, 1, 0, 3)
      if g_adj[i] > 0
        ui_2d_text(x + g_cell / 2, y + g_cell / 2 - 8, g_adj[i].to_s, ms_num_color(g_adj[i]), 14, VML_ANCHOR_CENTER)
      end
    end
    if s == 3
      ui_2d_rect(x + 1, y + 1, g_cell - 2, g_cell - 2, g_c_mine, 1, 0, 3)
      ui_2d_circle(x + g_cell / 2, y + g_cell / 2, g_cell / 4, g_c_bg, 1, 0)
    end
    i = i + 1
  end
  # 踩雷之后把剩下的雷摊开("我踩的是哪一颗"和"别的都在哪"一起看见)

  # After stepping on a mine, lay the remaining mines open (so "which one I hit" and "where the rest are" are visible together)

  if g_phase == 2
    i = 0
    while i < g_n
      if g_mine[i] == 1 && g_state[i] == 0
        r = i / g_cols
        c = i % g_cols
        x = g_bx + c * g_cell
        y = g_by + r * g_cell
        ui_2d_rect(x + 1, y + 1, g_cell - 2, g_cell - 2, g_c_panel, 1, 0, 3)
        ui_2d_circle(x + g_cell / 2, y + g_cell / 2, g_cell / 4, g_c_mine, 1, 0)
      end
      i = i + 1
    end
  end
  # 光标:一个描边的方框(键盘用它选格;触摸也会把它挪到点过的那一格,当"上一手"的记号)

  # The cursor: an outlined box (the keyboard uses it to pick a cell; a touch also moves it to the tapped cell, marking the last move)

  ui_2d_rect(g_bx + g_cur_c * g_cell, g_by + g_cur_r * g_cell, g_cell, g_cell, g_c_cursor, 0, 3, 4)
  # 下方两个按钮:旗标模式 / 重开

  # The two buttons below: flag mode and restart

  if g_flagmode == 1
    ui_2d_rect(g_btn1_x, g_btn_y, g_btn_w, g_btn_h, g_c_flag, 1, 0, 8)
    if g_lang == 0 then ui_2d_text(g_btn1_x + g_btn_w / 2, g_btn_y + 11, "旗标模式 开", g_c_bg, 13, VML_ANCHOR_CENTER) else ui_2d_text(g_btn1_x + g_btn_w / 2, g_btn_y + 11, "Flag mode ON", g_c_bg, 13, VML_ANCHOR_CENTER) end
  else
    ui_2d_rect(g_btn1_x, g_btn_y, g_btn_w, g_btn_h, g_c_panel, 1, 0, 8)
    if g_lang == 0 then ui_2d_text(g_btn1_x + g_btn_w / 2, g_btn_y + 11, "旗标模式 关", g_c_dim, 13, VML_ANCHOR_CENTER) else ui_2d_text(g_btn1_x + g_btn_w / 2, g_btn_y + 11, "Flag mode OFF", g_c_dim, 13, VML_ANCHOR_CENTER) end
  end
  ui_2d_rect(g_btn2_x, g_btn_y, g_btn_w, g_btn_h, g_c_panel, 1, 0, 8)
  if g_lang == 0 then ui_2d_text(g_btn2_x + g_btn_w / 2, g_btn_y + 11, "重开一局", g_c_title, 13, VML_ANCHOR_CENTER) else ui_2d_text(g_btn2_x + g_btn_w / 2, g_btn_y + 11, "New game", g_c_title, 13, VML_ANCHOR_CENTER) end
  # 结局横幅:通关(绿)/ 踩雷(红)

  # The end-of-round banner: cleared (green) / stepped on a mine (red)

  if g_phase == 1
    ui_2d_rect(g_pad, g_by + g_bh / 2 - 26, g_w - g_pad * 2, 52, g_c_ok, 1, 0, 10)
    if g_lang == 0 then ui_2d_text(g_w / 2, g_by + g_bh / 2 - 12, "通关!按 R 再来一局", g_c_bg, 16, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_by + g_bh / 2 - 12, "Cleared! Press R", g_c_bg, 16, VML_ANCHOR_CENTER) end
  end
  if g_phase == 2
    ui_2d_rect(g_pad, g_by + g_bh / 2 - 26, g_w - g_pad * 2, 52, g_c_mine, 1, 0, 10)
    if g_lang == 0 then ui_2d_text(g_w / 2, g_by + g_bh / 2 - 12, "踩雷了,按 R 再来一局", g_c_bg, 16, VML_ANCHOR_CENTER) else ui_2d_text(g_w / 2, g_by + g_bh / 2 - 12, "Boom! Press R", g_c_bg, 16, VML_ANCHOR_CENTER) end
  end
  return 0
end

# ══════════════════════════════════════════════════════════════════════════

# 第二段:布局与状态(⚠ 全部写在 `def` 之后 —— 见文件头那条)

# Section 2: layout and state (⚠ all of it after the `def`s — see the header note)

# ══════════════════════════════════════════════════════════════════════════


# ── 配色(负数十进制 = 0xAARRGGBB,本前端的老写法,同 catch.rb)──

# ── Palette (negative decimals = 0xAARRGGBB, the old convention of this frontend, same as catch.rb) ──

# 依次是:背景 / 面板 / 正文 / 次要文字 / 未翻开格 / 已翻开格 / 旗标 / 雷 / 通关 / 光标

# In order: background / panel / body / secondary / closed cell / opened cell / flag / mine / cleared / cursor

g_c_bg = -15724520       # 0xFF101018

g_c_panel = -15066588    # 0xFF1A1A24

g_c_title = -1513232     # 0xFFE8E8F0

g_c_dim = -6643536       # 0xFF9AA0B0

g_c_hidden = -12959664   # 0xFF3A4050

g_c_open = -14341578     # 0xFF252A36

g_c_flag = -2509750      # 0xFFD9B44A

g_c_mine = -2075821      # 0xFFE05353

g_c_ok = -11483016       # 0xFF50C878

g_c_cursor = -8096       # 0xFFFFE060


# ── 布局:**开窗时传 0(尺寸交给宿主),开窗之后再问真实画布**,位置全部由 w / h 算出来(手机上 ui_scr_get_h 约 578)──

# ── 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 (on phones ui_scr_get_h is about 578) ──

g_lang = ui_scr_get_language()
if g_lang == 0 then ui_win_open("扫雷", 0, 0) else ui_win_open("Minesweeper", 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

# 格子边长取"宽能塞下 cols 格、高能塞下 rows 格"里的小者 —— 两种屏幕都不会被切

# The cell size takes the smaller of "cols fit across the width" and "rows fit down the height" — neither screen gets clipped

g_cols = 8
g_rows = 10
g_mines_total = 12
g_pad = 12
g_top = 56
g_avail_w = g_w - g_pad * 2
g_avail_h = g_h - g_top - 120
g_cell = g_avail_w / g_cols
if g_avail_h / g_rows < g_cell
  g_cell = g_avail_h / g_rows
end
if g_cell < 12
  g_cell = 12
end
g_bw = g_cell * g_cols
g_bh = g_cell * g_rows
g_bx = (g_w - g_bw) / 2
g_by = g_top
g_btn_h = 38
g_btn_y = g_by + g_bh + 14
if g_btn_y + g_btn_h > g_h - 6
  g_btn_y = g_h - 6 - g_btn_h
end
g_btn_w = (g_w - g_pad * 3) / 2
g_btn1_x = g_pad
g_btn2_x = g_pad * 2 + g_btn_w
g_n = g_cols * g_rows

# ── 状态:三个等长数组(雷 / 格子状态 / 周围雷数),下标一律 r * cols + c ──

# ── State: three equal-length arrays (mines / cell state / neighbour count), always indexed by r * cols + c ──

g_mine = []
g_state = []
g_adj = []
g_i = 0
while g_i < g_n
  g_mine.push(0)
  g_state.push(0)
  g_adj.push(0)
  g_i = g_i + 1
end
g_opened = 0
g_flags = 0
# 局面:0 = 进行中,1 = 通关,2 = 踩雷

# Phase: 0 = playing, 1 = cleared, 2 = stepped on a mine

g_phase = 0
# 第一次点击之前不布雷 ⇒ 第一下永远是安全的(所有扫雷都这么做)

# No mines are placed before the first tap ⇒ the first tap is always safe (every minesweeper does this)

g_first = 0
g_flagmode = 0
g_seed = 10068
g_game = 0
g_cur_r = 4
g_cur_c = 4
g_alive = 1

# ── 开局 ──

# ── Start playing ──

ms_new_game()

# 主循环:**没有定时器** —— 回合制游戏只在"有输入"时重画(省电,也符合本仓"刷新由变化驱动"那条)

# The main loop: **there is no timer** — a turn-based game only redraws when there is input (saves power, and follows this repo's "refresh is driven by change" rule)

while g_alive == 1
  ms_draw()
  ui_2d_present()
  t = ui_msg_fetch_wait(0)
  if t == 1
    k = ui_msg_get_a()
    if k == 27
      g_alive = 0
    end
    if k == 82 || k == 114
      ms_new_game()
    end
    if k == 32
      if g_flagmode == 0
        g_flagmode = 1
      else
        g_flagmode = 0
      end
    end
    if k == 70 || k == 102
      ms_flag(g_cur_r * g_cols + g_cur_c)
    end
    if k == 13
      ms_open(g_cur_r * g_cols + g_cur_c)
    end
    if k == 37
      if g_cur_c > 0
        g_cur_c = g_cur_c - 1
      end
    end
    if k == 39
      if g_cur_c < g_cols - 1
        g_cur_c = g_cur_c + 1
      end
    end
    if k == 38
      if g_cur_r > 0
        g_cur_r = g_cur_r - 1
      end
    end
    if k == 40
      if g_cur_r < g_rows - 1
        g_cur_r = g_cur_r + 1
      end
    end
  end
  if t == 6
    ms_tap(ui_msg_get_a(), ui_msg_get_b())
  end
  if t == 7
    # 拖动会产生大量 TOUCHMOVE;本游戏只认"点一下",把它们丢掉免得队列越排越长

    # Dragging produces a flood of TOUCHMOVE messages; this game only cares about a single tap, so drop them to keep the queue short

    ui_msg_drop(1)
  end
  if t == 10
    g_alive = 0
  end
  if ui_win_is_closed() != 0
    g_alive = 0
  end
end

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("minesweeper: board=")
print(g_cols)
print("x")
print(g_rows)
print(" mines=")
print(g_mines_total)
print(" cell=")
print(g_cell)
print("\n")
print("minesweeper: opened=")
print(g_opened)
print(" flags=")
print(g_flags)
print(" phase=")
print(g_phase)
print(" games=")
print(g_game)
print("\n")

# ── 收尾的库自检:把 Ruby 语言库这一轮修好的接口各用一遍 ──

# ── The library self-check at the end: exercise each interface the Ruby language library gained in this round ──

# 两个用处:① 给无头验收一行**确定性**的判据(画面只能肉眼看,数字能自动比);

# Two uses: ① give headless acceptance one **deterministic** line (pictures can only be eyeballed; numbers can be compared automatically);

# ② 这些方法在手机与桌面上走同一条路,跑一遍就知道有没有退化(本文件的翻格/插旗已经在用

# ② these methods take the same path on a phone and on a desktop, so one run shows any regression (this file already uses

#    `push` / `pop` / `length` / `clear`,这里补齐其余那些)。

#    `push` / `pop` / `length` / `clear` in the game itself; the rest are covered here).

# ⚠ 三条**已知不通**的写法这里一个都不用:`puts(3.5)`(浮点打印恒为 0)、`s[i] += "x"`、

# ⚠ Three **known-broken** forms are avoided here: `puts(3.5)` (float printing is always 0), `s[i] += "x"`,

#   以及把 `split` 的产物喂给 `join`(分片是 `char*`,而 `join` 收 `int*`,会把指针当整数打)。

#   and feeding `split` output into `join` (the pieces are `char*` while `join` takes `int*`, so the pointer is printed as an integer).

a = [5, 3, 9, 1]
b = a.sort
c = b.reverse
print("libself: array=")
print(b[0])
print(",")
print(c[0])
print(",")
print(a.min)
print(",")
print(a.max)
print(",")
print(a.index(9))
print(",")
print(a.include?(3))
print(",")
a.push(7)
print(a.pop)
print(",")
a.unshift(2)
print(a.shift)
print(",")
a.insert(0, 8)
print(a.delete_at(0))
print(",")
print(a.take(1)[0])
print(",")
print(a.drop(0)[0])
print(",")
print(a.empty?)
print(",")
d = [1, 2]
d.concat([3, 4])
print(d.length)
print(",")
d.clear
print(d.length)
print("\n")
s = "  Hello World  "
t = s.strip
u = "Hello World".upcase
v = u.reverse
print("libself: str=")
print("123abc".to_i)
print(",")
print(456.to_s)
print(",")
print("ab".repeat(3))
print(",")
print("A".ord)
print(",")
print("aBc".swapcase)
print(",")
print("a,b,c".split(",").length)
print(",")
print(t.length)
print(",")
print(v)
print(",")
print("Hello World".downcase)
print(",")
print("Hello World".index("World"))
print(",")
print("Hello World".include?("World"))
print(",")
print("Hello World".start_with?("Hello"))
print(",")
print("Hello World".end_with?("World"))
print("\n")
# 内联汇编也走一遍(`SYSCALL #6` 打印 R0 里的整数,与 file_io.rb 同一条路)

# Inline assembly gets a turn too (`SYSCALL #6` prints the integer in R0, the same path as file_io.rb)

print("libself: asm=")
asm("MOVE R0 #42")
asm("SYSCALL 6")
print("\n")
print("minesweeper: done\n")