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