From ff14dffb6753336f5d7bff99069bb054e159bad1 Mon Sep 17 00:00:00 2001 From: QIU SHENGMING Date: Thu, 1 Oct 2026 22:29:36 +0800 Subject: [PATCH] Fix the blank screen, correct the multiboot note, and add Minesweeper The blank screen was self-inflicted and the earlier explanation was wrong. The FMP3 marker at 0x100000 is an ordinary state record -- generation, image_size, image_crc32, firmware_slot/slot_inv, config_bank/bank_inv and a matching state_crc32 (0x661286F1) -- not a pending flag. What actually happened: a firmware uploaded over a state that recorded a different identity made the firmware take the restore/adopt path, which draws nothing (panel 1024/1024 bytes zero) while the serial banner printed normally. Restoring the untouched dump fixed it at once (485/1024 bytes lit) and the three apps reinstalled and were confirmed by 0x0730. Clearing the marker sectors was tried twice (a whole 8 KiB, then just the 24-byte headers) and is not a fix: it sends the firmware down MB_MARK_MISSING = fresh radio, which adopts the running firmware slowly and without drawing, and its own write-back restores the marker anyway. AGENTS.md and AGENTS.zh-CN.md now say so in place of the wrong claim. apps/minesweeper/ adds our own 9x9 minesweeper for the 4 KiB overlay: no left/right keys exist on this radio (so the cursor walks with UP/DOWN and digits pick a row then a column), 81 cells need three 9-byte bit arrays rather than a uint16_t mask (the uint16_t version compiled fine and was wrong past cell 15), mines are placed after the first reveal so it cannot lose immediately, and the source compiles clean under gcc -Wall -Wextra -Werror against upstream's real app_api.h. It has not been built for ARM or run -- no toolchain here -- and the README says so. --- AGENTS.md | 32 +-- AGENTS.zh-CN.md | 22 +- apps/minesweeper/README.md | 46 ++++ apps/minesweeper/build.sh | 32 +++ apps/minesweeper/minesweeper_app.c | 343 +++++++++++++++++++++++++++++ 5 files changed, 452 insertions(+), 23 deletions(-) create mode 100644 apps/minesweeper/README.md create mode 100644 apps/minesweeper/build.sh create mode 100644 apps/minesweeper/minesweeper_app.c diff --git a/AGENTS.md b/AGENTS.md index 0a96765..a4cb58c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -641,21 +641,25 @@ the multiboot menu reads firmware slots rather than apps. **The answer was writt the flasher's own translation file**, not in the firmware source I had been reading -- so when a feature's entry point is missing, the host tool that installs it is the document. -**"Init ... DO NOT POWER OFF" that never ends is a pending multiboot state marker.** Measured -on a real image: `0x100000` held `FMP3` next to a committed slot 0, so the factory bootloader -reflashed the internal flash from that slot on every power-on and reset again -- a restore loop. -The firmware's own banner reappeared once a cycle in the serial log (7 -> 8 in 25 s) while the -screen never changed. Clearing the two marker sectors (`0x100000..0x101FFF`, which stops just -before the app region at `0x102000`, so installed apps are untouched) ended it: the banner count -stopped rising and the radio booted once and stayed. +**A blank screen after a firmware upload: check the multiboot state, and roll the image back -- +do not edit state by hand.** Measured here: an image whose `FMP3` marker at `0x100000` recorded one +identity (size 120832, CRC `0x4D87CE48`) while `-kernel` loaded a different build makes the firmware +decide the running image is not the one its state expects, so it takes the restore/adopt path and +draws nothing: the panel's whole 1024 bytes stayed zero while the serial banner printed happily. +Replacing the working copy with the untouched dump brought the picture straight back (485 of 1024 +bytes lit) and left the three installed apps reinstallable. -That loop explained a second surprise: edits made through the page vanished. The emulator writes -its in-memory image back when it exits, and the looping guest's copy was older than the file, so -powering it off overwrote what had just been installed. With the loop gone the same installs -survive a power cycle -- verified by installing, powering off, seeing them still listed, powering -on, seeing them still listed, and having the radio answer `0x0730` with all three. **A stale -write-back is worth suspecting whenever an edit "does not stick"**, and a guest that is quietly -rebooting is exactly how one happens. +Two corrections to what an earlier version of this section claimed. **The marker is not a pending +flag**: decode it and `generation`, `image_size`, `image_crc32`, `firmware_slot`/`slot_inv`, +`config_bank`/`bank_inv` and `state_crc32` all check out (`0x661286F1` over the first 20 bytes) -- +it is an ordinary state record. And **clearing the marker sectors is not a fix**: it was tried twice +(a whole 8 KiB, then only the 24-byte headers), after which the firmware took the `MB_MARK_MISSING` += "fresh radio" path, adopted the running firmware, drew nothing while doing it, and had the marker +written back by its own write-back anyway. Rolling the image back is what worked. + +**A stale write-back is still worth suspecting when an edit "does not stick"**: the emulator writes +its in-memory image back on exit, so an edit made while a guest was live can be overwritten by the +copy that guest was holding -- which is also how the marker reappeared after being cleared. ## The keypad: two real bugs, both fixed diff --git a/AGENTS.zh-CN.md b/AGENTS.zh-CN.md index 80cce8b..ce7def4 100644 --- a/AGENTS.zh-CN.md +++ b/AGENTS.zh-CN.md @@ -520,16 +520,20 @@ UVStudio 的 `locales/en.js` 原文是 "launch them from the F + 7 menu" —— app 区被读过 —— 而多系统菜单读的是**固件槽**,不是应用。**答案写在刷机工具自己的翻译文件里**, 不在我一直在读的固件源码里 —— 所以当一个功能的入口找不到时,**安装它的那个主机工具就是文档**。 -**永远停不下来的 "Init ... DO NOT POWER OFF" 是一个待执行的多系统状态标记。** 在真实镜像上量到: -`0x100000` 里是 `FMP3`,而槽 0 已提交 —— 于是出厂引导**每次开机都从槽 0 重刷内部 flash 再重启**, -成了恢复循环。串口日志里固件横幅**每个周期出现一次**(25 秒里 7 → 8),而屏幕一直不动。清掉那两个 -标记扇区(`0x100000..0x101FFF`,正好停在应用区 `0x102000` 之前,所以**已安装的应用不受影响**) -之后循环结束:横幅数不再增长,电台启动一次就稳定了。 +**上传固件之后白屏:先查多系统状态,然后回滚镜像 —— 不要手工去改状态。** 实测:镜像里 `0x100000` 的 +`FMP3` 标记记的是一个身份(size 120832、CRC `0x4D87CE48`),而 `-kernel` 加载的是另一份构建,于是固件 +判定"正在运行的镜像不是我的状态所期望的那份",走恢复/收养路径,**什么都不画**:面板 1024 字节全为 0, +而串口横幅照常打印。把工作副本换回未改动的 dump,画面立刻回来(1024 字节里 485 字节点亮),三个已安装的 +应用也可以重新装回。 -这个循环还解释了第二件怪事:**通过页面装的东西会消失**。模拟器退出时会把内存里的镜像写回文件, -而循环中的客人手里那份比文件更旧,于是"断电"这一下就把刚装进去的覆盖掉了。循环消失后,同一批安装 -**扛得住断电重上电** —— 实测:装上、断电仍在、上电仍在,并且电台用 `0x0730` 确认了三个都在。 -**每当"改了不生效",都值得怀疑一次过期的写回**,而一个在悄悄重启的客人正是它发生的典型方式。 +对本节早先版本的两处更正。**这个标记不是"待执行标志"**:解出来看,`generation`、`image_size`、 +`image_crc32`、`firmware_slot`/`slot_inv`、`config_bank`/`bank_inv` 与 `state_crc32` 全部自洽 +(前 20 字节的 CRC 为 `0x661286F1`)—— 它只是一条普通的状态记录。**而清掉标记扇区不是修复手段**:试过两次 +(先清整片 8 KiB,再只清 24 字节头),之后固件走的是 `MB_MARK_MISSING` = "新电台"那条路,会去收养当前 +运行的固件,过程很慢、不画屏,而且**它的写回又把标记写回来了**。真正有效的是**回滚镜像**。 + +**而当"改了不生效"时,过期的写回依然值得怀疑**:模拟器退出时会把内存里的镜像写回文件,所以客人还活着 +时做的改动,可能被它手里那份副本覆盖 —— 这也是标记被清掉后又重新出现的原因。 ## 键盘:两个真 bug,都已修复 diff --git a/apps/minesweeper/README.md b/apps/minesweeper/README.md new file mode 100644 index 0000000..f6f834b --- /dev/null +++ b/apps/minesweeper/README.md @@ -0,0 +1,46 @@ +# Minesweeper — an overlay app for the F4HWN Labs edition + +Our own app: a 9x9 minesweeper that runs on the radio inside the 4 KiB overlay that +`App/apps/app_overlay.h` reserves. It is written against upstream's `App/apps/app_api.h` +and built with upstream's `app.ld` — **neither is vendored here** (both are Apache-2.0 +files from [armel/uv-k1-k5v3-firmware-custom](https://github.com/armel/uv-k1-k5v3-firmware-custom)), +so drop this folder into `App/apps/minesweeper/` next to them, or point `-I` at a copy. + +## Why it looks the way it does + +| constraint | consequence | +| --- | --- | +| the radio has **no left/right keys** (UP, DOWN, MENU, EXIT, STAR, F, 0-9 only) | the cursor walks the field with UP/DOWN and digits jump to a row then a column: `3` `5` = row 3, column 5 | +| **4 KiB** for text+rodata+data+bss together | no lookup tables, no floats, no libc; adjacency is counted on the fly and each cell is one bit | +| 81 cells do not fit a 16-bit mask | three 9-byte bit arrays addressed by `cell >> 3`, `cell & 7` — a `uint16_t` version compiled fine and was wrong past cell 15 | +| the resident pixel helpers do **not** bound-check | `put()`/`invert()` clip | +| no `rand()` in a freestanding blob | a small LCG; mines are placed **after the first reveal**, keeping the 3x3 around it clear | + +Keys: UP/DOWN move, 1-9 pick row then column, MENU reveal, F flag, STAR new game, +EXIT quit. `M` in the corner is the remaining-mine count, `A1` is the cursor. + +## Build + + arm-none-eabi-gcc -mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding \ + -nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=0x20000280 \ + -o minesweeper.elf minesweeper_app.c + arm-none-eabi-objcopy -O binary minesweeper.elf minesweeper.bin + pack_app.py minesweeper.bin Minesweeper.app --name Minesweeper --ver 1.0 \ + --vma 0x20000280 --api-min 1 # pack_app.py lives in App/apps/ + +`build.sh` does exactly that and needs the Arm GNU Toolchain on PATH. + +Then install it **from the page**: *Overlay apps* → a slot → pick `Minesweeper.app` → +**Install** → **Ask the radio** should answer `Minesweeper`. On the radio press +**F** then **7** and **MENU** to run it. + +## What is verified, and what is not + +Verified here: the source compiles clean with `gcc -Wall -Wextra -Werror` against +upstream's real `app_api.h` (that check caught the API's actual member names — +`api->fb`, `print_tiny(s, x, y, statusbar, fill)` with **five** arguments — and the fact +that `APP_KEY_LEFT`/`APP_KEY_RIGHT` do not exist). + +Not verified: it has never been built for ARM or run on the radio, because no +`arm-none-eabi-gcc` and no Docker exist on the machine it was written on. Treat the +first build and the first run as the real review. diff --git a/apps/minesweeper/build.sh b/apps/minesweeper/build.sh new file mode 100644 index 0000000..6d58690 --- /dev/null +++ b/apps/minesweeper/build.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Build one overlay-app blob (.app) locally, without Docker. +# +# Upstream runs this same file inside the uvk1-uvk5v3 image; it already prefers +# arm-none-eabi-gcc from PATH when there is one, which is what makes a local build +# possible. Link VMA is pinned at 0x20000280 and must match the firmware's +# __mb_workspace_start -- the loader compares them and refuses with APP_VMA. +set -euo pipefail + +APP="$(basename "$PWD")" +APP_NAME="Minesweeper" # <-- the only per-app line: the label in the 64-byte header +APP_VER="1.0" +APP_API_MIN=1 +APP_VMA=${APP_VMA:-0x20000280} +OUT="${APP_NAME// /}" + +CC=${CC:-arm-none-eabi-gcc} +OBJCOPY=${OBJCOPY:-arm-none-eabi-objcopy} +command -v "$CC" >/dev/null 2>&1 || { + echo "no $CC on PATH; install the Arm GNU Toolchain (arm-none-eabi) first" >&2; exit 2; } + +CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \ + -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra" +LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ + -Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none" + +rm -f ./*.app ./*.elf ./*.bin +"$CC" $CFLAGS $LDFLAGS -o "${APP}.elf" "${APP}_app.c" +"$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +python3 pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" \ + --vma "$APP_VMA" --api-min "$APP_API_MIN" +ls -l "${OUT}.app" diff --git a/apps/minesweeper/minesweeper_app.c b/apps/minesweeper/minesweeper_app.c new file mode 100644 index 0000000..2adaa17 --- /dev/null +++ b/apps/minesweeper/minesweeper_app.c @@ -0,0 +1,343 @@ +/* Minesweeper — overlay app for the F4HWN Labs edition. + * + * Written against App/apps/app_api.h. The constraints shaped it: + * - the radio has NO left/right keys (UP, DOWN, MENU, EXIT, STAR, F, 0-9 only), so the + * cursor walks the field with UP/DOWN and digits jump straight to a row and column + * - 4 KiB for text+rodata+data+bss together: no lookup tables, no floats, no libc, + * adjacency counted on the fly, one bit per cell + * - the resident pixel helpers do not bound-check, so put() clips + * + * Keys: UP/DOWN move the cursor, 1-9 pick row then column (3 then 5 = row 3, col 5), + * MENU reveal, F flag, STAR new game, EXIT quit. The first reveal is always safe: + * the mines are placed after it, keeping the 3x3 around it clear. + * + * The 81 cells do not fit in any single integer type this chip shifts cheaply, so the + * three cell sets are 9-byte bit arrays addressed by cell index (cell >> 3, cell & 7). + * A uint16_t version of this compiled fine and would have been wrong past cell 15. + */ +#include +#include +#include "../app_api.h" + +#define N 9 /* 9x9: cell pitch 6 px -> 54x54 on a 128x64 screen */ +#define PITCH 6 +#define FIELD_X 4 +#define FIELD_Y 10 +#define MINES 10 +#define CELLS (N * N) +#define BYTES ((CELLS + 7) / 8) + +static const app_api_t *A; + +static uint8_t g_mine[BYTES]; +static uint8_t g_open[BYTES]; +static uint8_t g_flag[BYTES]; +static uint8_t g_cursor; +static uint8_t g_pending; /* 0 = no digit typed, 1 = row typed */ +static uint8_t g_row_pick; +static uint8_t g_placed; +static uint8_t g_state; /* 0 play, 1 lost, 2 won */ +static int8_t g_mine_left; + +static bool bit(const uint8_t *set, uint8_t cell) +{ + return ((set[cell >> 3] >> (cell & 7)) & 1u) != 0; +} + +static void setbit(uint8_t *set, uint8_t cell, bool on) +{ + uint8_t mask = (uint8_t)(1u << (cell & 7)); + if (on) + set[cell >> 3] |= mask; + else + set[cell >> 3] &= (uint8_t)~mask; +} + +static void put(int16_t x, int16_t y, bool ink) +{ + if (x < 0 || x >= 128 || y < 0 || y >= 64) + return; /* the API's helpers do not clip */ + if (ink) + A->fb[y][x >> 3] |= (uint8_t)(1u << (7u - (x & 7))); + else + A->fb[y][x >> 3] &= (uint8_t)~(1u << (7u - (x & 7))); +} + +static void invert(int16_t x, int16_t y) +{ + if (x < 0 || x >= 128 || y < 0 || y >= 64) + return; + A->fb[y][x >> 3] ^= (uint8_t)(1u << (7u - (x & 7))); +} + +static void box(int16_t x0, int16_t y0, int16_t x1, int16_t y1, bool ink) +{ + for (int16_t x = x0; x <= x1; x++) { + put(x, y0, ink); + put(x, y1, ink); + } + for (int16_t y = y0; y <= y1; y++) { + put(x0, y, ink); + put(x1, y, ink); + } +} + +static int8_t neighbours(uint8_t cell) +{ + int8_t row = (int8_t)(cell / N); + int8_t col = (int8_t)(cell % N); + int8_t count = 0; + + for (int8_t dy = -1; dy <= 1; dy++) { + for (int8_t dx = -1; dx <= 1; dx++) { + int8_t r = (int8_t)(row + dy); + int8_t c = (int8_t)(col + dx); + if ((dx == 0 && dy == 0) || r < 0 || r >= N || c < 0 || c >= N) + continue; + if (bit(g_mine, (uint8_t)(r * N + c))) + count++; + } + } + return count; +} + +static void place_mines(uint8_t safe) +{ + /* No rand() in a freestanding blob, so a cheap LCG. Seeded from the first reveal and + * a counter so two games in a row differ. */ + static uint32_t seed = 1u; + int8_t want = MINES; + + for (uint8_t i = 0; i < BYTES; i++) + g_mine[i] = 0; + + seed += (uint32_t)safe * 2654435761u + 1u; + while (want > 0) { + seed = seed * 1103515245u + 12345u; + uint8_t cell = (uint8_t)((seed >> 16) % CELLS); + int8_t row = (int8_t)(cell / N), col = (int8_t)(cell % N); + int8_t srow = (int8_t)(safe / N), scol = (int8_t)(safe % N); + + if (bit(g_mine, cell)) + continue; + /* the 3x3 around the first reveal stays clear, so it cannot lose at once */ + if (row >= srow - 1 && row <= srow + 1 && col >= scol - 1 && col <= scol + 1) + continue; + setbit(g_mine, cell, true); + want--; + } + g_placed = 1; +} + +static void reveal(uint8_t cell) +{ + /* Iterative flood fill: an explicit stack of cell indices, no recursion. */ + static uint8_t stack[CELLS]; + int16_t top = 0; + + stack[top++] = cell; + while (top > 0) { + uint8_t cur = stack[--top]; + + if (bit(g_open, cur)) + continue; + setbit(g_open, cur, true); + if (neighbours(cur) != 0) + continue; + + int8_t row = (int8_t)(cur / N), col = (int8_t)(cur % N); + for (int8_t dy = -1; dy <= 1; dy++) { + for (int8_t dx = -1; dx <= 1; dx++) { + int8_t r = (int8_t)(row + dy), c = (int8_t)(col + dx); + if (r < 0 || r >= N || c < 0 || c >= N) + continue; + uint8_t next = (uint8_t)(r * N + c); + if (!bit(g_open, next) && !bit(g_flag, next) && top < CELLS) + stack[top++] = next; + } + } + } +} + +static void new_game(void) +{ + for (uint8_t i = 0; i < BYTES; i++) { + g_mine[i] = 0; + g_open[i] = 0; + g_flag[i] = 0; + } + g_cursor = (uint8_t)(4 * N + 4); + g_pending = 0; + g_row_pick = 0; + g_placed = 0; + g_state = 0; + g_mine_left = MINES; +} + +static void step(int8_t delta) +{ + int8_t cell = (int8_t)((int8_t)g_cursor + delta); + if (cell < 0) + cell = (int8_t)(CELLS - 1); + if (cell >= CELLS) + cell = 0; + g_cursor = (uint8_t)cell; +} + +static void two_digits(char *out, int8_t value) +{ + if (value < 0) + value = 0; + if (value > 99) + value = 99; + out[0] = (char)('0' + (value / 10) % 10); + out[1] = (char)('0' + value % 10); + out[2] = 0; +} + +static void draw(void) +{ + char text[3]; + + A->display_clear(); + + A->print_tiny("M", 0, 1, false, false); + two_digits(text, g_mine_left); + A->print_tiny(text, 8, 1, false, false); + + if (g_state == 1) + A->print_tiny("BOOM", 46, 1, false, false); + else if (g_state == 2) + A->print_tiny("CLEAR", 42, 1, false, false); + else + A->print_tiny("F4HWN MINES", 34, 1, false, false); + + text[0] = (char)('A' + (g_cursor / N)); + text[1] = (char)('1' + (g_cursor % N)); + text[2] = 0; + A->print_tiny(text, 110, 1, false, false); + if (g_pending) + A->print_tiny("-", 122, 1, false, false); + + for (uint8_t cell = 0; cell < CELLS; cell++) { + int16_t x = (int16_t)(FIELD_X + (cell % N) * PITCH); + int16_t y = (int16_t)(FIELD_Y + (cell / N) * PITCH); + bool opened = bit(g_open, cell); + bool mine = bit(g_mine, cell); + /* mines show once the game is over, whether or not they were flagged */ + bool show_mine = mine && (g_state != 0 || opened); + + if (show_mine) { + box(x, y, (int16_t)(x + 4), (int16_t)(y + 4), true); + put((int16_t)(x + 2), (int16_t)(y + 2), false); + } else if (opened) { + int8_t n = neighbours(cell); + if (n > 0) { + text[0] = (char)('0' + n); + text[1] = 0; + A->print_tiny(text, (uint8_t)x, (uint8_t)y, false, false); + } + } else if (bit(g_flag, cell)) { + box((int16_t)(x + 1), (int16_t)(y + 1), (int16_t)(x + 3), (int16_t)(y + 3), true); + put((int16_t)(x + 2), (int16_t)(y + 4), true); + } + } + + /* the cursor inverts the frame around its cell, so it shows on ink and on paper */ + int16_t cx = (int16_t)(FIELD_X + (g_cursor % N) * PITCH - 1); + int16_t cy = (int16_t)(FIELD_Y + (g_cursor / N) * PITCH - 1); + for (int16_t i = 0; i <= PITCH + 1; i++) { + invert((int16_t)(cx + i), cy); + invert((int16_t)(cx + i), (int16_t)(cy + PITCH + 1)); + invert(cx, (int16_t)(cy + i)); + invert((int16_t)(cx + PITCH + 1), (int16_t)(cy + i)); + } + + A->blit_full(); +} + +static void check_win(void) +{ + int16_t closed = 0; + + for (uint8_t cell = 0; cell < CELLS; cell++) + if (!bit(g_open, cell) && !bit(g_mine, cell)) + closed++; + if (closed == 0) { + g_state = 2; + A->play_tone(880, 120); + A->play_tone(1320, 160); + } +} + +void app_main(const app_api_t *api) +{ + A = api; + new_game(); + + while (true) { + draw(); + + uint8_t key = A->get_key(); + if (key == APP_KEY_INVALID || key == APP_KEY_SAVER) { + A->delay_ms(40); + continue; + } + if (key == APP_KEY_EXIT) + return; /* the loader restores the radio */ + if (key == APP_KEY_STAR) { + new_game(); + continue; + } + if (g_state != 0) { /* after BOOM or CLEAR, MENU restarts */ + if (key == APP_KEY_MENU) + new_game(); + continue; + } + if (key == APP_KEY_UP) { + step(-1); + continue; + } + if (key == APP_KEY_DOWN) { + step(1); + continue; + } + if (key >= APP_KEY_1 && key <= APP_KEY_9) { + uint8_t digit = (uint8_t)(key - APP_KEY_0); /* 1..9 */ + if (!g_pending) { + g_row_pick = digit; /* row first ... */ + g_pending = 1; + } else { + g_cursor = (uint8_t)((g_row_pick - 1) * N + (digit - 1)); /* ... then column */ + g_pending = 0; + } + continue; + } + if (key == APP_KEY_F) { /* flag */ + if (!bit(g_open, g_cursor)) { + if (bit(g_flag, g_cursor)) { + setbit(g_flag, g_cursor, false); + g_mine_left++; + } else if (g_mine_left > 0) { + setbit(g_flag, g_cursor, true); + g_mine_left--; + } + } + continue; + } + if (key == APP_KEY_MENU) { /* reveal */ + if (bit(g_flag, g_cursor)) + continue; + if (!g_placed) + place_mines(g_cursor); + if (bit(g_mine, g_cursor)) { + setbit(g_open, g_cursor, true); + g_state = 1; + A->play_tone(160, 400); + continue; + } + reveal(g_cursor); + check_win(); + } + } +}