Refactor Minesweeper: API v2 assets, title screen, bug fixes

This commit is contained in:
Armel FAUVEAU committed 2026-09-27 00:34:54 +02:00
1 parent b1dde27536
commit 2aff80cc0e
5 files changed
+588 -449

No files matched your search

+1
View File
@@ -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
+4 -17
View File
@@ -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.*) }
}
+17 -17
View File
@@ -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")
+191
View File
@@ -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)
+375 -415
View File
@@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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);
}