mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Refactor Minesweeper: API v2 assets, title screen, bug fixes
This commit is contained in:
1 parent
b1dde27536
commit
2aff80cc0e
5 files changed
+568
-429
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.*) }
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Executable
+191
@@ -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)
|
||||
@@ -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 LCD_WIDTH 128
|
||||
#define LCD_HEIGHT 64
|
||||
#define W 128u
|
||||
#define COLS 15u
|
||||
#define ROWS 6u
|
||||
#define TILE 8u
|
||||
#define MINES 12u
|
||||
#define TICK_MS 40u
|
||||
#define BOARD_WIDTH 15
|
||||
#define BOARD_HEIGHT 6
|
||||
#define CFG_MAGIC 0x5361u
|
||||
|
||||
typedef struct {
|
||||
uint16_t magic;
|
||||
uint16_t version;
|
||||
uint32_t seed;
|
||||
} config_t;
|
||||
/* 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
|
||||
|
||||
static const app_api_t *A;
|
||||
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");
|
||||
|
||||
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). */
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
/* 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++;
|
||||
/* 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++;
|
||||
}
|
||||
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';
|
||||
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 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++;
|
||||
}
|
||||
}
|
||||
/* 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);
|
||||
}
|
||||
|
||||
// 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;}
|
||||
/* 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);
|
||||
|
||||
uint8_t mines_around = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
for (int8_t nx = -1; nx <= 1; nx++) {
|
||||
for (int8_t ny = -1; ny <= 1; ny++) {
|
||||
/* 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();
|
||||
}
|
||||
|
||||
if (nx == 0 && ny == 0) {continue;}
|
||||
/* 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 (x + nx < 0 || x + nx >= BOARD_WIDTH ||
|
||||
y + ny < 0 || y + ny >= BOARD_HEIGHT) {
|
||||
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 (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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
generateMines();
|
||||
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];
|
||||
}
|
||||
|
||||
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));
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawCursor(void) {
|
||||
if (cursor_blink < 9) {
|
||||
drawSprite(cursor_x + 1, cursor_y + 1, cursor_sprite);
|
||||
}
|
||||
}
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Input, screensaver handling and the main loop */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void key_action(uint8_t key, bool repeat) {
|
||||
static void key_action(uint8_t key, bool repeat)
|
||||
{
|
||||
if (key == APP_KEY_EXIT) {
|
||||
isRunning = false;
|
||||
g.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cursor_blink = 0;
|
||||
|
||||
if (gameLoss) {
|
||||
renderLoss();
|
||||
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) {
|
||||
initGame();
|
||||
}
|
||||
return;
|
||||
|
||||
} else if (gameWin) {
|
||||
renderWin();
|
||||
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) {
|
||||
initGame();
|
||||
}
|
||||
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_STAR) {
|
||||
currentFace = NORMAL;
|
||||
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();
|
||||
}
|
||||
|
||||
int8_t dx = 0;
|
||||
int8_t dy = 0;
|
||||
|
||||
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;
|
||||
|
||||
} else if (key == APP_KEY_F) { // Flagging
|
||||
toggleFlag(cursor_x / 8, cursor_y / 8);
|
||||
|
||||
if (mines_left == 0) {
|
||||
gameWin = true;
|
||||
}
|
||||
|
||||
} else if (key == APP_KEY_STAR) { // Revealing
|
||||
currentFace = SCARED;
|
||||
|
||||
if (board[cursor_x / 8][cursor_y / 8] & MINE_BIT) {
|
||||
gameLoss = 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;}
|
||||
}
|
||||
|
||||
void poll_key(void) {
|
||||
static void poll_key(void)
|
||||
{
|
||||
uint8_t key = A->get_key();
|
||||
|
||||
if (key == APP_KEY_INVALID) {
|
||||
previousKey = key;
|
||||
repeatMs = 0;
|
||||
if (key == APP_KEY_SAVER) {
|
||||
g.saverActive = true;
|
||||
g.previousKey = APP_KEY_INVALID;
|
||||
g.repeatMs = 0;
|
||||
return;
|
||||
}
|
||||
if (key != previousKey) {
|
||||
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);
|
||||
repeatMs = 260u;
|
||||
} else if (repeatMs > TICK_MS) {
|
||||
repeatMs -= TICK_MS;
|
||||
g.repeatMs = 260u;
|
||||
} else if (g.repeatMs > TICK_MS) {
|
||||
g.repeatMs -= TICK_MS;
|
||||
} else {
|
||||
key_action(key, true);
|
||||
repeatMs = 80u;
|
||||
g.repeatMs = 80u;
|
||||
}
|
||||
previousKey = key;
|
||||
g.previousKey = key;
|
||||
}
|
||||
|
||||
/* ---- entry point, pinned to blob offset 0 ---- */
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api) {
|
||||
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;
|
||||
|
||||
randSeed = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ seed;
|
||||
g.previousKey = APP_KEY_INVALID;
|
||||
g.running = true; /* the rest of g starts zeroed (.bss): the title first */
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
|
||||
initGame();
|
||||
|
||||
isRunning = true;
|
||||
|
||||
while(isRunning) {
|
||||
while (g.running) {
|
||||
poll_key();
|
||||
if (!g.running)
|
||||
break;
|
||||
|
||||
if (!gameWin && !gameLoss) {
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
|
||||
drawField();
|
||||
draw3dEffect();
|
||||
drawCursor();
|
||||
drawStatus();
|
||||
|
||||
A->delay_ms(TICK_MS);
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user