mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 11:07:31 +00:00
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.
344 lines
10 KiB
C
344 lines
10 KiB
C
/* 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 <stdint.h>
|
|
#include <stdbool.h>
|
|
#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();
|
|
}
|
|
}
|
|
}
|