mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 11:08:20 +00:00
425 lines
14 KiB
C
425 lines
14 KiB
C
/* 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
|
|
|
|
/* 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
|
|
|
|
enum { ST_TITLE, ST_PLAY, ST_WON, ST_LOST };
|
|
|
|
_Static_assert(MINES <= COLS * ROWS - 9u, "the first reveal needs a 3x3 area free of mines");
|
|
|
|
/* 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");
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Texts, the tile sheet, the smileys, the cursor steps, the board background
|
|
* and the title screen are read-only assets (gen_assets.py). */
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Board logic */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
}
|
|
return opened;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
flood();
|
|
tally();
|
|
}
|
|
|
|
/* 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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
static void render(void)
|
|
{
|
|
uint8_t sheet[TILES_LEN];
|
|
const bool lost = g.state == ST_LOST;
|
|
|
|
/* 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));
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (g.state == ST_PLAY && g.blink < 9u)
|
|
put_tile(g.cx, g.cy, &sheet[K_CURSOR * TILE_W], 0u);
|
|
|
|
/* 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);
|
|
|
|
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];
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
__attribute__((section(".text.entry"), used))
|
|
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();
|
|
|
|
while (g.running) {
|
|
poll_key();
|
|
if (!g.running)
|
|
break;
|
|
|
|
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);
|
|
}
|
|
|
|
A->led(false);
|
|
}
|