diff --git a/App/apps/app_art.py b/App/apps/app_art.py index 304c314f..81d54229 100644 --- a/App/apps/app_art.py +++ b/App/apps/app_art.py @@ -31,6 +31,7 @@ LOGO = { "S": (9, [[(9, 0), (2, 0), (0, 2), (0, 4), (2, 6), (7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]), "T": (10, [[(0, 0), (10, 0)], [(5, 0), (5, 13)]]), "U": (9, [[(0, 0), (0, 10), (3, 13), (6, 13), (9, 10), (9, 0)]]), + "W": (12, [[(0, 0), (0, 13), (6, 7), (12, 13), (12, 0)]]), } LOGO_KX, LOGO_KY = 1.0, 1.1 # default skeleton scale LOGO_BRUSH = 3 # stroke thickness in pixels diff --git a/App/apps/minesweeper/app.ld b/App/apps/minesweeper/app.ld index b7439278..41f94ce7 100644 --- a/App/apps/minesweeper/app.ld +++ b/App/apps/minesweeper/app.ld @@ -1,19 +1,6 @@ -/* Overlay-app link script (POC). - * - * The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with - * the multiboot RAM stub — never both active at once). Code + rodata + data + - * bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+ - * data only; the loader zeroes the overlay before copying, so bss starts clean. - * - * VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM - * layout shifts, update APP_VMA and the loader's compile-time assert catches a - * mismatch. */ - -/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It - * varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x... - * The default is only a fallback for a standalone size check. */ +/* Minesweeper overlay app: code, constants, data and BSS share the 4 KiB workspace. */ APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280; -APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */ +APP_LENGTH = 0x1000; ENTRY(app_main) @@ -23,7 +10,7 @@ MEMORY { SECTIONS { .app APP_VMA : { - KEEP(*(.text.entry)) /* app_main pinned to offset 0 */ + KEEP(*(.text.entry)) *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); @@ -37,7 +24,7 @@ SECTIONS { __app_end = .; ASSERT(__app_end <= APP_VMA + APP_LENGTH, - "Breakout overlay app overflows the 4 KiB overlay") + "Minesweeper overlay app overflows the 4 KiB overlay") /DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) } } diff --git a/App/apps/minesweeper/build.sh b/App/apps/minesweeper/build.sh index c3f0a986..89579658 100755 --- a/App/apps/minesweeper/build.sh +++ b/App/apps/minesweeper/build.sh @@ -1,36 +1,36 @@ #!/usr/bin/env bash -# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside -# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is -# derived from the directory name; only APP_NAME - the human label baked into -# the 64-byte blob header - is per-app. The .app file is named after it (spaces -# stripped), so "Broadcast FM" -> BroadcastFM.app. set -euo pipefail -APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... -APP_NAME="Minesweeper" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 -APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) -OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) +APP="$(basename "$PWD")" +APP_NAME="Minesweeper" +APP_VER="1.1" +APP_API_MIN=2 +APP_VMA=${APP_VMA:-0x20000280} +OUT="${APP_NAME// /}" CC=/opt/toolchain/bin/arm-none-eabi-gcc OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; } +command -v "$CC" >/dev/null 2>&1 || { echo "❌ ARM compiler not found: $CC"; exit 1; } +command -v "$OBJCOPY" >/dev/null 2>&1 || { echo "❌ ARM objcopy not found: $OBJCOPY"; exit 1; } + 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 -Wl,--no-warn-rwx-segments" -rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous +rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r 🔨 %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" >/dev/null +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/minesweeper/gen_assets.py b/App/apps/minesweeper/gen_assets.py new file mode 100755 index 00000000..dffd44a3 --- /dev/null +++ b/App/apps/minesweeper/gen_assets.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# Minesweeper read-only assets: texts, the tile sheet, the status-line +# smileys, the cursor steps of keys 1..9, the board background and the +# 128x64 title screen. +# +# Tile sheet: one record of TILE_W column bytes per kind (LSB = top row), +# already placed inside the tile: column i is x + 1 + i and bit b is row +# y + b of the tile's frame-buffer page. render() ORs a record in with no +# shift, and loads the whole sheet onto its stack once per frame. +# +# ./gen_assets.py minesweeper_assets.bin minesweeper_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas, W + +COLS, ROWS, TILE = 15, 6, 8 # the board, as in minesweeper_app.c +FB_PAGES = 7 # frame-buffer pages under the status line +TILE_W = 7 + +def cols(rows): + """Picture rows ('#' = pixel) -> column bytes, LSB = top row.""" + return [sum(1 << r for r, row in enumerate(rows) if row[c] == "#") + for c in range(len(rows[0]))] + +DIGITS = [[0x02, 0x1F, 0x00], [0x19, 0x15, 0x12], # gFont3x5 '1' .. '8', the + [0x11, 0x15, 0x0A], [0x07, 0x04, 0x1F], # glyphs print_tiny draws + [0x17, 0x15, 0x09], [0x1E, 0x15, 0x1D], + [0x19, 0x05, 0x03], [0x1F, 0x15, 0x1F]] +FLAG = cols([".##..", # pennant on a pole: unlike + ".###.", # the digit 1 next to it + ".####", + ".#...", + "###.."]) +MINE = cols(["...#...", # round, four horns and a + "..###..", # highlight, as the title's + ".#.###.", # big mine + "#######", + ".#####.", + "..###..", + "...#..."]) +BOOM = [cols(["...#...", # the mine that went off: a + ".#.#.#.", # white star on its black + "..###..", # cell, big then small (two + "#######", # frames) + "..###..", + ".#.#.#.", + "...#..."]), + cols([".......", + "...#...", + "..###..", + ".#####.", + "..###..", + "...#...", + "......."])] +CURSOR = cols(["##...##", # corner brackets + "#.....#", + ".......", + ".......", + ".......", + "#.....#", + "##...##"]) +FACES = [[0x00, 0x10, 0x22, 0x20, 0x20, 0x22, 0x10, 0x00], # playing + [0x00, 0x00, 0x32, 0x48, 0x48, 0x32, 0x00, 0x00], # STAR held + [0x02, 0x16, 0x26, 0x22, 0x22, 0x26, 0x16, 0x02], # won + [0x05, 0x22, 0x15, 0x10, 0x10, 0x15, 0x22, 0x05]] # lost + +def interior(sprite, dx, dy): + """A sprite drawn at (x + 1 + dx, y + dy) of its tile, as a sheet record.""" + out = [0] * TILE_W + for i, c in enumerate(sprite): + if c << dy > 0xFF: + raise SystemExit("tile sprite crosses the tile's page") + out[dx + i] = c << dy + return out + +TILES = [[0] * TILE_W] # 0: revealed, no mine around +TILES += [interior(d, 2, 2) for d in DIGITS] # 1..8: as print_tiny at (x + 3, y + 2) +K_FLAG, K_MINE, K_CURSOR, K_BOOM = range(len(TILES), len(TILES) + 4) +TILES += [interior(FLAG, 1, 2), # 5x5 at (x + 2, y + 2) + interior(MINE, 0, 1), interior(CURSOR, 0, 1)] # 7x7 at (x + 1, y + 1) +TILES += [interior(b, 0, 1) for b in BOOM] # K_BOOM, K_BOOM + 1 + +# The board in frame-buffer coordinates: it stays on page boundaries (tile row +# = page) right under the status line, BOARD_X px from the left. Its soft +# shadow is a 50 % dither SHADOW_R px wide right of the board and SHADOW_B px +# under it, SHADOW_GAP px away from it all along, as cast by a raised board: +# each band starts with a 45° cut in line with a corner of the board (top +# right, bottom left). +RIGHT, BOTTOM = COLS * TILE, ROWS * TILE +BOARD_X, SHADOW_GAP, SHADOW_R, SHADOW_B = 1, 1, 4, 3 +assert BOARD_X + RIGHT + SHADOW_GAP + SHADOW_R < W +assert BOTTOM + SHADOW_GAP + SHADOW_B < FB_PAGES * 8 + +def board_background(): + """The board outline and its soft shadow, as the frame-buffer pages + render() copies in place of a clear before drawing the tiles.""" + px = set() + left, right = BOARD_X, BOARD_X + RIGHT + for x in range(left, right + 1): # the outline, as draw_rect + px.add((x, 0)); px.add((x, BOTTOM)) + for y in range(BOTTOM + 1): + px.add((left, y)); px.add((right, y)) + edge_x, edge_y = right + SHADOW_GAP, BOTTOM + SHADOW_GAP # last blank column / row + for y in range(edge_y + SHADOW_B + 1): # the shadow, off the board + for x in range(left, edge_x + SHADOW_R + 1): + side = x > edge_x and y >= x - right # right band, cut in line with (right, 0) + under = y > edge_y and x >= left + y - BOTTOM # bottom band, in line with (left, BOTTOM) + if (side or under) and (x + y) % 2 == 0: + px.add((x, y)) + return bytes(sum(1 << b for b in range(8) if (x, page * 8 + b) in px) + for page in range(FB_PAGES) for x in range(W)) + +# Cursor step (dx, dy) of keys 1..9: the keypad as a compass, 5 stays put. +DIRS = [-1, -1, 0, -1, 1, -1, + -1, 0, 0, 0, 1, 0, + -1, 1, 0, 1, 1, 1] + +def tile(cv, kind, x, y, hidden): + """A tile as render() draws it, its top-left corner at screen (x, y).""" + if hidden: + for k in range(TILE + 1): + cv.set(x + k, y); cv.set(x + k, y + TILE); cv.set(x, y + k); cv.set(x + TILE, y + k) + for i, c in enumerate(TILES[kind]): + for bit in range(8): + if c >> bit & 1: + cv.set(x + 1 + i, y + bit) + +def big_mine(cv, cx, cy): + """Round sea mine: an 11 px disc, eight horns and a highlight.""" + for y in range(-5, 6): + for x in range(-5, 6): + if x * x + y * y <= 30: # r * r + r: a round pixel disc + cv.set(cx + x, cy + y) + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + for k in (6, 7): + cv.set(cx + dx * k, cy + dy * k) + for dx, dy in ((-1, -1), (1, -1), (-1, 1), (1, 1)): # short, off the corners + cv.set(cx + dx * 4, cy + dy * 4) + for x, y in ((-2, -3), (-3, -2), (-2, -2)): + cv.set(cx + x, cy + y, 0) + +def big_flag(cv, x, y): + """A 14 px flag: pole, pennant pointing right, stepped base.""" + for k in range(13): + cv.set(x + 3, y + k) + for r, w in enumerate([3, 5, 7, 9, 7, 5, 3]): + for k in range(1, w + 1): + cv.set(x + 3 + k, y + r) + for k in range(1, 6): + cv.set(x + k, y + 12) + for k in range(7): + cv.set(x + k, y + 13) + +def title_screen(): + """MINE / SWEEPER between a big mine and a big flag, over a row of + tiles as the game draws them (screen y = frame-buffer y + 8).""" + cv = Canvas() + cv.two_words("MINE", "SWEEPER") + big_mine(cv, 16, 10) + big_flag(cv, 101, 3) + for i, t in enumerate("HHHF1..12FHHHHH"): # Hidden, Flag, '.' empty, digits + kind = K_FLAG if t == "F" else int(t) if t.isdigit() else 0 + tile(cv, kind, 4 + i * 8, 46, t in "HF") + return cv + +preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None +a = Assets("MINESWEEPER") +a.text("T_FLAGS", "FLAGS LEFT:") +a.text("T_WIN", "YOU WIN!") +a.text("T_LOST", "YOU LOST!") +a.text("T_PRESS", "PRESS MENU") +a.u8("TILES", sum(TILES, [])) +a.const("TILE_W", TILE_W) +a.const("K_FLAG", K_FLAG) +a.const("K_MINE", K_MINE) +a.const("K_CURSOR", K_CURSOR) +a.const("K_BOOM", K_BOOM) +a.u8("FACE", sum(FACES, [])) +a.const("FACE_W", len(FACES[0])) +for i, name in enumerate(("FACE_PLAY", "FACE_SCARED", "FACE_WON", "FACE_LOST")): + a.const(name, i) +a.i8("DIRS", DIRS) +a.raw("ART_BOARD", board_background()) +a.const("BOARD_X", BOARD_X) +title = title_screen() +a.raw("ART_TITLE", title.pages()) +a.main() +if preview_path: + title.preview(preview_path) diff --git a/App/apps/minesweeper/minesweeper_app.c b/App/apps/minesweeper/minesweeper_app.c index a1a15eb7..31a7385a 100644 --- a/App/apps/minesweeper/minesweeper_app.c +++ b/App/apps/minesweeper/minesweeper_app.c @@ -1,464 +1,424 @@ +/* Copyright 2026 Sarijen https://github.com/Sarijen + * Armel F4HWN https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Minesweeper - overlay game for the 128x64 monochrome LCD. + * + * A 15x6 field of 8 px tiles on a board with a soft drop shadow, with the + * flags left and the smiley in the status line. The 12 mines are laid at the + * first reveal, away from the revealed tile and its neighbours, so the first + * move is always safe. The game is won when every safe tile is revealed, or + * when every mine carries a flag (there are only as many flags as mines). + * + * Keys: 1-9 move the cursor (eight directions, wrapping around the edges), + * STAR reveals, F places or removes a flag, MENU / STAR / 0 start a game + * from the title or after the end, EXIT quits. + */ + #include #include #include #include "../app_api.h" +#include "minesweeper_assets.h" /* generated by gen_assets.py */ +#define W 128u +#define COLS 15u +#define ROWS 6u +#define TILE 8u +#define MINES 12u +#define TICK_MS 40u -#define LCD_WIDTH 128 -#define LCD_HEIGHT 64 -#define TICK_MS 40u -#define BOARD_WIDTH 15 -#define BOARD_HEIGHT 6 -#define CFG_MAGIC 0x5361u +/* Tile byte: the count of neighbouring mines (0..8) in the low nibble. */ +#define COUNT 0x0Fu +#define FLAG 0x10u +#define MINE 0x20u +#define HIDDEN 0x40u -typedef struct { - uint16_t magic; - uint16_t version; - uint32_t seed; -} config_t; +enum { ST_TITLE, ST_PLAY, ST_WON, ST_LOST }; -static const app_api_t *A; +_Static_assert(MINES <= COLS * ROWS - 9u, "the first reveal needs a 3x3 area free of mines"); - -static uint8_t board[BOARD_WIDTH][BOARD_HEIGHT]; - -#define FLAG_BIT (1 << 4) -#define MINE_BIT (1 << 5) -#define HIDDEN_BIT (1 << 6) - -/* -Board tile bit layout - Flag - | Mine - | | Hidden - | | | - | | | - 1 1 1 1000 - Mines around (0-8) -*/ - -static uint32_t randSeed, seed; -static bool isRunning, gameWin, gameLoss; -static uint8_t previousKey; -static uint16_t repeatMs; -static uint8_t mines_left, flags_left; -static char status_str[15]; -static uint8_t cursor_x, cursor_y; -static uint8_t cursor_blink = 0; - -static const uint8_t mine_sprite[8] = { 0x15, 0x0E, 0x1F, 0x0E, 0x15, 0x00, 0x00, 0x00 }; -static const uint8_t flag_sprite[8] = { 0x14, 0x16, 0x1F, 0x10, 0x10, 0x00, 0x00, 0x00 }; -static const uint8_t cursor_sprite[8] = { 0x63, 0x41, 0x00, 0x00, 0x00, 0x41, 0x63, 0x00 }; - -typedef enum { - NORMAL, - SCARED, - COOL, - DEAD, -} smileyFace; - -smileyFace currentFace = NORMAL; - -static const uint8_t smiley_sprites[4][8] = { - { 0x00, 0x10, 0x22, 0x20, 0x20, 0x22, 0x10, 0x00 }, // Normal - { 0x00, 0x00, 0x32, 0x48, 0x48, 0x32, 0x00, 0x00 }, // Scared - { 0x02, 0x16, 0x26, 0x22, 0x22, 0x26, 0x16, 0x02 }, // Cool - { 0x05, 0x22, 0x15, 0x10, 0x10, 0x15, 0x22, 0x05 }, // Dead +/* All the state in one struct: Thumb-1 code then reaches every field from a + * single base address (one literal per function instead of one per global), + * byte fields first so their offsets fit ldrb's 0..31 immediate. */ +struct globals { + uint8_t state, cx, cy, flagsLeft, previousKey, blink, tick; + bool running, mined, saverActive; + uint16_t repeatMs; + const app_api_t *api; + uint8_t board[ROWS][COLS]; }; +static struct globals g; +#define A (g.api) +_Static_assert(offsetof(struct globals, saverActive) < 32u, "byte fields out of ldrb range"); -static int rand_custom(void) { - randSeed = randSeed * 1103515245u + 12345u; - return (randSeed >> 16) & 0x7FFF; +/* GCC may lower aggregate clears to memset even for this freestanding blob. */ +void *memset(void *dst, int value, size_t size) +{ + uint8_t *p = dst; + while (size--) + *p++ = (uint8_t)value; + return dst; } -static void invertRect(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { - for (uint8_t y = y1; y <= y2; y++) { - for (uint8_t x = x1; x <= x2; x++) { - A->fb[y/8][x] ^= (1 << (y % 8)); - } - } -} +/* Texts, the tile sheet, the smileys, the cursor steps, the board background + * and the title screen are read-only assets (gen_assets.py). */ -// Draw 8x8 sprite -static void drawSprite(uint8_t x, uint8_t y, const uint8_t* sprite) { - uint8_t pixel_data; - uint8_t bit_position = (y % 8); - uint8_t page = y / 8; +/* -------------------------------------------------------------------------- */ +/* Board logic */ +/* -------------------------------------------------------------------------- */ - for (uint8_t byte = 0; byte < 8; byte++) { - pixel_data = sprite[byte]; - - A->fb[page][x + byte] |= (0xFF & (pixel_data << bit_position)); - - if ((8 - bit_position) < 0) { - A->fb[page+1][x + byte] |= (pixel_data >> (8 - bit_position)); - } - } -} - -/* zero-padded unsigned -> string, width w (<= 5). Replaces sprintf. */ -static void u2str(char *out, const char *label, uint16_t v, uint8_t w) { - char *o = out; - if (label != NULL) { - while (*label) *o++ = *label++; - } - char tmp[6]; int8_t n = 0; - do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v && n < 6); - while (n < w) tmp[n++] = '0'; - while (n--) *o++ = tmp[n]; - *o = '\0'; -} - -static void generateMines(void) { - mines_left = 0; - flags_left = 0; - - for (uint8_t x = 0; x < BOARD_WIDTH; x++) { - for (uint8_t y = 0; y < BOARD_HEIGHT; y++) { - board[x][y] |= HIDDEN_BIT; - - if (x == 0 && y == 0) {continue;} - - if (rand_custom() % 5 == 0) { - board[x][y] |= MINE_BIT; - mines_left++; - flags_left++; - } - } - } - - // Calculate neighbouring mines - for (uint8_t x = 0; x < BOARD_WIDTH; x++) { - for (uint8_t y = 0; y < BOARD_HEIGHT; y++) { - if (board[x][y] & MINE_BIT) {continue;} - - uint8_t mines_around = 0; - - for (int8_t nx = -1; nx <= 1; nx++) { - for (int8_t ny = -1; ny <= 1; ny++) { - - if (nx == 0 && ny == 0) {continue;} - - if (x + nx < 0 || x + nx >= BOARD_WIDTH || - y + ny < 0 || y + ny >= BOARD_HEIGHT) { - continue; - } - - if (board[x + nx][y + ny] & MINE_BIT) { - mines_around++; - } - } - } - - board[x][y] |= mines_around; - } - } -} - -static void initGame(void) { - A->led(false); - - cursor_x = 0; - cursor_y = 0; - gameWin = false; - gameLoss = false; - currentFace = NORMAL; - - for (uint8_t x = 0; x < BOARD_WIDTH; x++) { - for (uint8_t y = 0; y < BOARD_HEIGHT; y++) { - board[x][y] = HIDDEN_BIT; - } - } - - generateMines(); -} - -static void drawField(void) { - for (uint8_t x = 0; x < BOARD_WIDTH*8; x += 8) { - for (uint8_t y = 0; y < BOARD_HEIGHT*8; y += 8) { - uint8_t cur_tile = board[x/8][y/8]; - - if (cur_tile & HIDDEN_BIT) { - A->draw_rect(A->fb, x, y, x + 8, y + 8, true); - } - - if (cur_tile & FLAG_BIT) { - drawSprite(x + 2, y + 2, flag_sprite); - - } else if ((cur_tile & 0x0F) > 0 && !(cur_tile & HIDDEN_BIT)) { - - char num_str[1]; - u2str(num_str, NULL, (cur_tile & 0x0F), 1); - A->print_tiny(num_str, x + 3, y + 2, false, true); - } - } - } - - // Board edges - A->draw_line(A->fb, 0, 0, 120, 0, true); - A->draw_line(A->fb, 0, 0, 0, 48, true); - - A->draw_line(A->fb, 120, 0, 120, 48, true); - A->draw_line(A->fb, 0, 48, 120, 48, true); - - A->draw_line(A->fb, 7, 55, 127, 55, true); - A->draw_line(A->fb, 127, 7, 127, 55, true); -} - -static void draw3dEffect(void) { - for (uint8_t x = 0; x < 7; x++) { - A->fb[6][x] |= (0b1 << (x % 8)); - A->fb[0][121 + x] |= (0b10 << (x % 8)); - A->fb[6][121 + x] |= (0b10 << (x % 8)); - } -} - -static void drawCursor(void) { - if (cursor_blink < 9) { - drawSprite(cursor_x + 1, cursor_y + 1, cursor_sprite); - } -} - -static void drawStatus(void) { - u2str(status_str, "FLAGS LEFT: ", flags_left, 2); - A->print_tiny(status_str, 1, 1, true, true); - - for (uint8_t byte = 0; byte < 8; byte++) { - A->status_line[65 + byte] |= smiley_sprites[currentFace][byte]; - } -} - -static void renderLoss(void) { - A->display_clear(); - A->status_clear(); - - drawField(); - draw3dEffect(); - currentFace = DEAD; - drawStatus(); - - for (uint8_t x = 0; x < BOARD_WIDTH*8; x += 8) { - for (uint8_t y = 0; y < BOARD_HEIGHT*8; y += 8) { - - if (board[x/8][y/8] & MINE_BIT) { - // Reveal remaining mines - if (!(board[x/8][y/8] & FLAG_BIT)) { - drawSprite(x + 2, y + 2, mine_sprite); - } - - } else { - // Mark wrongly placed flags - if (board[x/8][y/8] & FLAG_BIT) { - invertRect(x + 1, y + 1, x + 7, y + 7); +/* Visit tile (x, y) and its neighbours inside the board. With open false, add + * one to their mine count (the centre is a mine: its own count is never + * shown). With open true, reveal the hidden, unflagged ones (the centre is a + * revealed tile with no mine around, so none of them is a mine); returns + * true when a tile opened. */ +static bool around(uint32_t x, uint32_t y, bool open) +{ + bool opened = false; + for (uint32_t ny = y - 1u; ny != y + 2u; ny++) { + for (uint32_t nx = x - 1u; nx != x + 2u; nx++) { + if (nx >= COLS || ny >= ROWS) /* unsigned: also catches -1 */ + continue; + uint8_t *t = &g.board[ny][nx]; + if (!open) { + (*t)++; + } else if ((*t & (HIDDEN | FLAG)) == HIDDEN) { + *t &= (uint8_t)~HIDDEN; + opened = true; + } } - } } - } - - // Mark the hit mine - invertRect(cursor_x + 1, cursor_y + 1, cursor_x + 7, cursor_y + 7); - - u2str(status_str, "FLAGS LEFT: ", flags_left, 2); - A->print_tiny(status_str, 1, 1, true, true); - A->print_tiny("YOU LOST!", 80, 1, true, true); - - A->blit_status(); - A->blit_full(); + return opened; } -static void renderWin(void) { - A->led(true); - drawField(); - draw3dEffect(); - currentFace = COOL; - drawStatus(); - - A->print_tiny("YOU WIN!", 80, 1, true, true); - A->blit_status(); - A->blit_full(); +/* Lay the mines outside the 3x3 area around the first revealed tile (x, y), + * then count each tile's neighbouring mines. */ +static void lay_mines(uint32_t x, uint32_t y) +{ + for (uint32_t n = 0; n < MINES; ) { + const uint32_t r = A->rand32(); /* resident PRNG, seeded by the loader */ + const uint32_t mx = ((r >> 24) * COLS) >> 8; + const uint32_t my = (((r >> 16) & 0xFFu) * ROWS) >> 8; + uint8_t *t = &g.board[my][mx]; + if ((*t & MINE) || (mx + 1u - x <= 2u && my + 1u - y <= 2u)) + continue; + *t |= MINE; + n++; + } + for (uint32_t my = 0; my < ROWS; my++) + for (uint32_t mx = 0; mx < COLS; mx++) + if (g.board[my][mx] & MINE) + around(mx, my, false); + g.mined = true; } -static void toggleFlag(uint8_t x, uint8_t y) { - if (!(board[x][y] & HIDDEN_BIT) || flags_left == 0) {return;} - - if (board[x][y] & FLAG_BIT) { - flags_left++; - } else { - flags_left--; - } - - if (board[x][y] & MINE_BIT) { - mines_left--; - } else { - mines_left++; - } - - board[x][y] ^= FLAG_BIT; +/* Open the neighbours of every revealed empty tile until nothing changes. */ +static void flood(void) +{ + bool opened; + do { + opened = false; + for (uint32_t y = 0; y < ROWS; y++) + for (uint32_t x = 0; x < COLS; x++) + if (!(g.board[y][x] & (HIDDEN | MINE | COUNT)) && around(x, y, true)) + opened = true; + } while (opened); } -static void revealTiles(int8_t x, int8_t y) { - if (x < 0 || x >= BOARD_WIDTH || - y < 0 || y >= BOARD_HEIGHT) { - return; - } - - // Skip if already revealed or flag placed - if (!(board[x][y] & HIDDEN_BIT) || board[x][y] & FLAG_BIT) { - return; - } else { - board[x][y] &= ~HIDDEN_BIT; - } - - if (board[x][y] & MINE_BIT || board[x][y] & 0x0F) { - return; - } - - for (int8_t nx = -1; nx <= 1; nx++) { - for (int8_t ny = -1; ny <= 1; ny++) { - if (nx == 0 && ny == 0) {continue;} - - revealTiles(x + nx, y + ny); +/* Recount the flags from the board and detect a win: no safe tile left + * hidden, or every mine flagged (flags never outnumber the mines). The won + * board is shown solved: mines flagged, safe tiles revealed. */ +static void tally(void) +{ + uint8_t *t = (uint8_t *)g.board; /* the whole board as ROWS * COLS bytes */ + uint32_t flags = 0, flaggedMines = 0, hiddenSafe = 0; + for (uint32_t i = 0; i < ROWS * COLS; i++) { + if (t[i] & FLAG) { + flags++; + if (t[i] & MINE) + flaggedMines++; + } + if ((t[i] & (HIDDEN | MINE)) == HIDDEN) + hiddenSafe++; } - } + g.flagsLeft = (uint8_t)(MINES - flags); + + if (hiddenSafe != 0u && flaggedMines != MINES) + return; + for (uint32_t i = 0; i < ROWS * COLS; i++) + t[i] = (t[i] & MINE) ? (uint8_t)(t[i] | FLAG) : (uint8_t)(t[i] & ~HIDDEN); + g.flagsLeft = 0; + g.state = ST_WON; + A->led(true); } -static void key_action(uint8_t key, bool repeat) { - if (key == APP_KEY_EXIT) { - isRunning = false; - return; - } - - cursor_blink = 0; - - if (gameLoss) { - renderLoss(); - - if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) { - initGame(); +/* STAR: reveal the tile under the cursor (a flag protects it). */ +static void reveal(void) +{ + uint8_t *t = &g.board[g.cy][g.cx]; + if ((*t & (HIDDEN | FLAG)) != HIDDEN) + return; + if (!g.mined) + lay_mines(g.cx, g.cy); + *t &= (uint8_t)~HIDDEN; + if (*t & MINE) { + g.state = ST_LOST; + return; } - return; + flood(); + tally(); +} - } else if (gameWin) { - renderWin(); +/* F: flag or unflag the hidden tile under the cursor. Removing a flag is + * always allowed; placing one needs a flag left. */ +static void toggle_flag(void) +{ + uint8_t *t = &g.board[g.cy][g.cx]; + if (!(*t & HIDDEN) || (!(*t & FLAG) && g.flagsLeft == 0u)) + return; + *t ^= FLAG; + tally(); +} - if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) { - initGame(); +static void new_game(void) +{ + memset(g.board, HIDDEN, sizeof(g.board)); + g.cx = COLS / 2u; + g.cy = ROWS / 2u; + g.flagsLeft = MINES; + g.mined = false; + g.blink = 0; + g.state = ST_PLAY; + A->led(false); +} + +/* -------------------------------------------------------------------------- */ +/* Rendering */ +/* -------------------------------------------------------------------------- */ + +/* OR a tile-sheet record into the interior of tile (x, y), then XOR invert + * (0xFE inverts rows 1..7, the whole interior). */ +static void put_tile(uint32_t x, uint32_t y, const uint8_t *spr, uint32_t invert) +{ + uint8_t *col = &A->fb[y][BOARD_X + x * TILE + 1u]; + for (uint32_t i = 0; i < TILE_W; i++) + col[i] = (uint8_t)((col[i] | spr[i]) ^ invert); +} + +/* The mine that went off: its whole cell, bars included, in black with the + * burst record in white, whatever its neighbours draw around it. Its bottom + * bar is row 0 of the next page. */ +static void boom(uint32_t x, uint32_t y, const uint8_t *spr) +{ + uint8_t *col = &A->fb[y][BOARD_X + x * TILE]; + uint8_t *below = &A->fb[y + 1u][BOARD_X + x * TILE]; + for (uint32_t i = 0; i <= TILE; i++) { + col[i] = (i == 0u || i == TILE) ? 0xFFu : (uint8_t)~spr[i - 1u]; + below[i] |= 1u; } - return; - } +} - if (key != APP_KEY_STAR) { - currentFace = NORMAL; - } +/* A status-line text from the assets at x. */ +static void status_text(uint16_t off, uint8_t x) +{ + char buf[TEXT_MAX]; + A->asset_read(off, buf, TEXT_MAX); + A->print_tiny(buf, x, 1, true, true); +} - int8_t dx = 0; - int8_t dy = 0; +static void render(void) +{ + uint8_t sheet[TILES_LEN]; + const bool lost = g.state == ST_LOST; - if (key == APP_KEY_1) { // UP LEFT - dx = -8; - dy = -8; - } else if (key == APP_KEY_2) { // UP - dy = -8; - } else if (key == APP_KEY_3) { // UP RIGHT - dx = 8; - dy = -8; - } else if (key == APP_KEY_4) { // LEFT - dx = -8; - } else if (key == APP_KEY_6) { // RIGHT - dx = 8; - } else if (key == APP_KEY_7) { // DOWN LEFT - dx = -8; - dy = 8; - } else if (key == APP_KEY_8) { // DOWN - dy = 8; - } else if (key == APP_KEY_9) { // DOWN RIGHT - dx = 8; - dy = 8; + /* The board background (outline and soft shadow) replaces the whole + frame buffer: it clears it at the same time. */ + A->status_clear(); + A->asset_read(ART_BOARD, A->fb[0], ART_BOARD_LEN); + A->asset_read(TILES, sheet, sizeof(sheet)); - } else if (key == APP_KEY_F) { // Flagging - toggleFlag(cursor_x / 8, cursor_y / 8); - - if (mines_left == 0) { - gameWin = true; + for (uint32_t y = 0; y < ROWS; y++) { + for (uint32_t x = 0; x < COLS; x++) { + const uint32_t t = g.board[y][x]; + const uint32_t px = BOARD_X + x * TILE; + uint32_t kind = 0; + if (t & HIDDEN) + A->draw_rect(A->fb, px, y * TILE, px + TILE, y * TILE + TILE, true); + if ((t & (MINE | HIDDEN)) == MINE) { /* the one that went off: it pulses, + 16 frames (640 ms) per image */ + boom(x, y, &sheet[(K_BOOM + ((g.tick >> 4) & 1u)) * TILE_W]); + continue; + } + if (t & FLAG) + kind = K_FLAG; + else if (t & MINE) + kind = lost ? K_MINE : 0u; /* shown once lost */ + else if (!(t & HIDDEN)) + kind = t & COUNT; + /* Once lost, invert the flags placed on safe tiles. */ + const bool mark = lost && (t & (FLAG | MINE)) == FLAG; + put_tile(x, y, &sheet[kind * TILE_W], mark ? 0xFEu : 0u); + } } - } else if (key == APP_KEY_STAR) { // Revealing - currentFace = SCARED; + if (g.state == ST_PLAY && g.blink < 9u) + put_tile(g.cx, g.cy, &sheet[K_CURSOR * TILE_W], 0u); - if (board[cursor_x / 8][cursor_y / 8] & MINE_BIT) { - gameLoss = true; - } + /* Status line: flags left, the smiley, the end-of-game message. */ + char digits[3]; + uint32_t v = g.flagsLeft, tens = '0'; + while (v >= 10u) { + v -= 10u; + tens++; + } + digits[0] = (char)tens; + digits[1] = (char)('0' + v); + digits[2] = '\0'; + status_text(T_FLAGS, 1); + A->print_tiny(digits, 49, 1, true, true); - revealTiles(cursor_x / 8, cursor_y / 8); - } - - - cursor_x = (cursor_x + dx + 120) % 120; - cursor_y = (cursor_y + dy + 48) % 48; - - if (repeat) {return;} + uint32_t face = g.previousKey == APP_KEY_STAR ? FACE_SCARED : FACE_PLAY; + if (g.state == ST_WON) { + face = FACE_WON; + status_text(T_WIN, 80); + } else if (lost) { + face = FACE_LOST; + status_text(T_LOST, 80); + } + uint8_t smiley[FACE_W]; + A->asset_read(FACE + face * FACE_W, smiley, FACE_W); + for (uint32_t i = 0; i < FACE_W; i++) + A->status_line[65u + i] |= smiley[i]; } -void poll_key(void) { - uint8_t key = A->get_key(); - - if (key == APP_KEY_INVALID) { - previousKey = key; - repeatMs = 0; - return; - } - if (key != previousKey) { - key_action(key, false); - repeatMs = 260u; - } else if (repeatMs > TICK_MS) { - repeatMs -= TICK_MS; - } else { - key_action(key, true); - repeatMs = 80u; - } - previousKey = key; +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers, + * with a blinking prompt on the blank bottom page. */ +static void draw_title(void) +{ + A->asset_read(ART_TITLE, A->status_line, W); + A->asset_read(ART_TITLE + W, A->fb[0], 7u * W); + if (g.tick & 16u) { + char buf[TEXT_MAX]; + A->asset_read(T_PRESS, buf, TEXT_MAX); + A->print_bold(buf, 0, W - 1, 6); + } +} + +/* -------------------------------------------------------------------------- */ +/* Input, screensaver handling and the main loop */ +/* -------------------------------------------------------------------------- */ + +static void key_action(uint8_t key, bool repeat) +{ + if (key == APP_KEY_EXIT) { + g.running = false; + return; + } + if (g.state != ST_PLAY) { /* title or game over: the start keys begin a game */ + if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) + new_game(); + return; + } + g.blink = 0; /* keep the cursor visible while it moves */ + + if (key >= APP_KEY_1 && key <= APP_KEY_9) { + int8_t d[2]; + A->asset_read(DIRS + (key - APP_KEY_1) * 2u, d, sizeof(d)); + int32_t x = (int32_t)g.cx + d[0]; + int32_t y = (int32_t)g.cy + d[1]; + if (x < 0) + x = COLS - 1u; + else if (x >= (int32_t)COLS) + x = 0; + if (y < 0) + y = ROWS - 1u; + else if (y >= (int32_t)ROWS) + y = 0; + g.cx = (uint8_t)x; + g.cy = (uint8_t)y; + return; + } + if (repeat) /* only the moves auto-repeat */ + return; + if (key == APP_KEY_STAR) + reveal(); + else if (key == APP_KEY_F) + toggle_flag(); +} + +static void poll_key(void) +{ + uint8_t key = A->get_key(); + if (key == APP_KEY_SAVER) { + g.saverActive = true; + g.previousKey = APP_KEY_INVALID; + g.repeatMs = 0; + return; + } + if (key == APP_KEY_WAKE || key == APP_KEY_PTT) { + g.saverActive = false; + key = APP_KEY_INVALID; + } + if (key == APP_KEY_INVALID) { + g.previousKey = key; + g.repeatMs = 0; + return; + } + if (key != g.previousKey) { + key_action(key, false); + g.repeatMs = 260u; + } else if (g.repeatMs > TICK_MS) { + g.repeatMs -= TICK_MS; + } else { + key_action(key, true); + g.repeatMs = 80u; + } + g.previousKey = key; } -/* ---- entry point, pinned to blob offset 0 ---- */ __attribute__((section(".text.entry"), used)) -void app_main(const app_api_t *api) { - A = api; - config_t cfg; - A->cfg_load((uint8_t *)&cfg, sizeof(cfg)); - seed = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.seed : 1u; +void app_main(const app_api_t *api) +{ + A = api; + g.previousKey = APP_KEY_INVALID; + g.running = true; /* the rest of g starts zeroed (.bss): the title first */ + A->led(false); + A->backlight_on(); - randSeed = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ seed; - A->led(false); - A->backlight_on(); + while (g.running) { + poll_key(); + if (!g.running) + break; - initGame(); - - isRunning = true; - - while(isRunning) { - poll_key(); - - if (!gameWin && !gameLoss) { - A->display_clear(); - A->status_clear(); - - drawField(); - draw3dEffect(); - drawCursor(); - drawStatus(); - - A->delay_ms(TICK_MS); - A->blit_status(); - A->blit_full(); + if (!g.saverActive) { /* the saver owns the screen: skip the frame */ + if (g.state == ST_TITLE) + draw_title(); + else + render(); + A->blit_status(); + A->blit_full(); + g.tick++; /* drives the title prompt and the explosion */ + if (++g.blink >= 20u) + g.blink = 0; + } + A->backlight_update(); + A->delay_ms(TICK_MS); } - cursor_blink++; - if (cursor_blink >= 20) {cursor_blink = 0;} - } - - - cfg.magic = CFG_MAGIC; - cfg.version = 1u; - cfg.seed = randSeed; - A->cfg_save((const uint8_t *)&cfg, sizeof(cfg)); + A->led(false); }