mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
556 lines
16 KiB
C
556 lines
16 KiB
C
/* Copyright 2026 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.
|
|
*/
|
|
|
|
/*
|
|
* Tetris — compact overlay game for the 128x64 monochrome LCD.
|
|
*
|
|
* The 16x16 visible well fills the screen height down the left side; two hidden
|
|
* spawn rows sit just above it. Rather than solid walls, the well is framed by
|
|
* short corner brackets and a floor line. The narrow right-hand panel shows the
|
|
* next piece, score, cleared lines, level and persistent best score. Pieces
|
|
* come from a shuffled seven-piece bag; a dotted ghost previews the landing.
|
|
*
|
|
* Keys: LEFT/RIGHT or 4/6 move, MENU or 2 rotates, 8 soft-drops,
|
|
* STAR or 0 hard-drops, F pauses and EXIT quits.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include "../app_api.h"
|
|
|
|
#define W 128u
|
|
#define H 64u
|
|
#define COLS 16
|
|
#define VISIBLE_ROWS 16
|
|
#define HIDDEN_ROWS 2
|
|
#define ROWS (VISIBLE_ROWS + HIDDEN_ROWS)
|
|
#define CELL 4
|
|
#define WELL_X 3 /* physical x of board column 0 */
|
|
#define WELL_Y 0 /* physical y of visible row 0 (full height) */
|
|
#define WELL_L 1 /* container left rail (corner brackets) */
|
|
#define WELL_R 67 /* container right rail (corner brackets) */
|
|
#define WELL_B 63 /* solid floor line, physical y */
|
|
#define LABEL_X 72 /* left column shared by every panel label */
|
|
#define PREVIEW_CX 108 /* centre of the NEXT-piece preview area */
|
|
#define PREVIEW_CY 15
|
|
#define TICK_MS 20u
|
|
#define CFG_MAGIC 0x5445u
|
|
|
|
typedef struct {
|
|
uint16_t magic;
|
|
uint16_t version;
|
|
uint32_t best;
|
|
} config_t;
|
|
|
|
static const app_api_t *A;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Four 4x4 masks for I, O, T, S, Z, J and L. */
|
|
static const uint16_t MASK[7][4] = {
|
|
{0x00F0, 0x4444, 0x0F00, 0x2222},
|
|
{0x0066, 0x0066, 0x0066, 0x0066},
|
|
{0x0072, 0x0262, 0x0270, 0x0232},
|
|
{0x0036, 0x0462, 0x0360, 0x0231},
|
|
{0x0063, 0x0264, 0x0630, 0x0132},
|
|
{0x0071, 0x0226, 0x0470, 0x0322},
|
|
{0x0074, 0x0622, 0x0170, 0x0223},
|
|
};
|
|
|
|
static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800};
|
|
static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u};
|
|
|
|
static uint16_t board[ROWS];
|
|
static uint8_t bag[7], bagPos;
|
|
static uint8_t piece, nextPiece, rotation;
|
|
static int8_t pieceX, pieceY;
|
|
static uint32_t randomState, score, best;
|
|
static uint16_t lines, gravityMs;
|
|
static uint8_t level;
|
|
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest;
|
|
static uint8_t previousKey;
|
|
static uint16_t repeatMs;
|
|
static char text[9];
|
|
|
|
static void render(bool showPiece, uint32_t clearRows, uint8_t effect);
|
|
static void new_game(void);
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* RNG and the shuffled 7-piece bag */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static uint32_t random_next(void)
|
|
{
|
|
randomState = randomState * 1664525u + 1013904223u;
|
|
return randomState;
|
|
}
|
|
|
|
static void refill_bag(void)
|
|
{
|
|
for (uint8_t i = 0; i < 7u; i++)
|
|
bag[i] = i;
|
|
for (uint8_t i = 6u; i > 0u; i--) {
|
|
uint8_t j;
|
|
do {
|
|
j = (uint8_t)(random_next() >> 29);
|
|
} while (j > i);
|
|
const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t;
|
|
}
|
|
bagPos = 0;
|
|
}
|
|
|
|
static uint8_t take_piece(void)
|
|
{
|
|
if (bagPos >= 7u)
|
|
refill_bag();
|
|
return bag[bagPos++];
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Framebuffer drawing: cells, pieces, side panel and dialogs */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static void set_pixel(int x, int y, bool on)
|
|
{
|
|
if ((unsigned)x >= W || (unsigned)y >= H)
|
|
return;
|
|
uint8_t *p;
|
|
if (y < 8)
|
|
p = &A->status_line[x];
|
|
else
|
|
p = &A->fb[(y >> 3) - 1][x];
|
|
const uint8_t bit = (uint8_t)(1u << (y & 7));
|
|
if (on) *p |= bit; else *p &= (uint8_t)~bit;
|
|
}
|
|
|
|
/* Short straight runs, in physical coordinates so they may cross the
|
|
* status-line / framebuffer seam that A->draw_line cannot. */
|
|
static void hline(int x0, int x1, int y)
|
|
{
|
|
for (int x = x0; x <= x1; x++) set_pixel(x, y, true);
|
|
}
|
|
|
|
static void vline(int x, int y0, int y1)
|
|
{
|
|
for (int y = y0; y <= y1; y++) set_pixel(x, y, true);
|
|
}
|
|
|
|
/* Corner brackets plus a solid floor — the well's frame, no vertical walls. */
|
|
static void container(void)
|
|
{
|
|
hline(WELL_L, WELL_R, WELL_B);
|
|
hline(WELL_L, WELL_L + 5, WELL_Y);
|
|
hline(WELL_R - 5, WELL_R, WELL_Y);
|
|
vline(WELL_L, WELL_Y, WELL_Y + 5);
|
|
vline(WELL_R, WELL_Y, WELL_Y + 5);
|
|
vline(WELL_L, WELL_B - 5, WELL_B);
|
|
vline(WELL_R, WELL_B - 5, WELL_B);
|
|
}
|
|
|
|
static void cell(int gx, int gy, uint8_t style)
|
|
{
|
|
const int visibleY = gy - HIDDEN_ROWS;
|
|
if ((unsigned)gx >= COLS || (unsigned)visibleY >= VISIBLE_ROWS)
|
|
return;
|
|
const int x = WELL_X + gx * CELL;
|
|
const int y = WELL_Y + visibleY * CELL;
|
|
if (style == 2u) {
|
|
set_pixel(x, y + 1, true);
|
|
set_pixel(x + 1, y, true);
|
|
set_pixel(x + 2, y + 1, true);
|
|
set_pixel(x + 1, y + 2, true);
|
|
} else {
|
|
const uint8_t size = style == 3u ? CELL : CELL - 1u;
|
|
for (uint8_t dy = 0; dy < size; dy++)
|
|
for (uint8_t dx = 0; dx < size; dx++)
|
|
set_pixel(x + dx, y + dy, true);
|
|
}
|
|
}
|
|
|
|
static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
|
|
{
|
|
const uint16_t mask = MASK[type][rot & 3u];
|
|
for (uint8_t i = 0; i < 16u; i++) {
|
|
if (!(mask & (uint16_t)(1u << i)))
|
|
continue;
|
|
const int x = px + (i & 3u);
|
|
const int y = py + (i >> 2);
|
|
if (x < 0 || x >= COLS || y >= ROWS)
|
|
return true;
|
|
if (y >= 0 && (board[y] & (uint16_t)(1u << x)))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style)
|
|
{
|
|
const uint16_t mask = MASK[type][rot & 3u];
|
|
for (uint8_t i = 0; i < 16u; i++)
|
|
if (mask & (uint16_t)(1u << i))
|
|
cell(px + (i & 3u), py + (i >> 2), style);
|
|
}
|
|
|
|
static void number(uint32_t value, uint8_t width)
|
|
{
|
|
const uint8_t first = (uint8_t)(6u - width);
|
|
for (uint8_t i = 0; i < width; i++) {
|
|
const uint32_t place = DECIMAL_PLACE[first + i];
|
|
uint8_t digit = 0;
|
|
while (value >= place) {
|
|
value -= place;
|
|
digit++;
|
|
}
|
|
text[i] = (char)('0' + digit);
|
|
}
|
|
text[width] = 0;
|
|
}
|
|
|
|
static void preview(void)
|
|
{
|
|
const uint16_t mask = MASK[nextPiece][0];
|
|
const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5;
|
|
const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3;
|
|
for (uint8_t i = 0; i < 16u; i++) {
|
|
if (!(mask & (uint16_t)(1u << i)))
|
|
continue;
|
|
const int x = ox + (i & 3u) * CELL;
|
|
const int y = oy + (i >> 2) * CELL;
|
|
for (uint8_t dy = 0; dy < CELL - 1u; dy++)
|
|
for (uint8_t dx = 0; dx < CELL - 1u; dx++)
|
|
set_pixel(x + dx, y + dy, true);
|
|
}
|
|
}
|
|
|
|
/* One "LABEL value" row of the side panel; y is framebuffer-relative
|
|
* (physical y - 8), value right-aligned near the screen edge. */
|
|
static void stat(const char *label, uint8_t y, uint32_t value,
|
|
uint8_t valueX, uint8_t width)
|
|
{
|
|
A->print_tiny(label, LABEL_X, y, false, true);
|
|
number(value, width);
|
|
A->print_tiny(text, valueX, y, false, true);
|
|
}
|
|
|
|
static void panel(void)
|
|
{
|
|
A->print_tiny("NEXT", LABEL_X, 2, false, true);
|
|
preview();
|
|
if (gameOver)
|
|
A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118);
|
|
else if (paused)
|
|
A->print_inverse("PAUSE", 90, 2, false, true, 110);
|
|
stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6);
|
|
stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3);
|
|
stat("LEVEL", 40, level, 117, 2);
|
|
stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6);
|
|
}
|
|
|
|
/* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */
|
|
static void render(bool showPiece, uint32_t clearRows, uint8_t effect)
|
|
{
|
|
A->display_clear();
|
|
A->status_clear();
|
|
A->print_inverse("TETRIS", 74, 0, true, true, 101);
|
|
|
|
container();
|
|
|
|
for (uint8_t y = 0; y < ROWS; y++)
|
|
for (uint8_t x = 0; x < COLS; x++)
|
|
if ((board[y] & (uint16_t)(1u << x)) && (!(clearRows & (1u << y)) ||
|
|
(x >= effect && x < COLS - effect)))
|
|
cell(x, y, 1u);
|
|
|
|
if (showPiece && !gameOver) {
|
|
int8_t ghostY = pieceY;
|
|
while (!collision(piece, rotation, pieceX, ghostY + 1))
|
|
ghostY++;
|
|
if (ghostY != pieceY)
|
|
draw_piece(piece, rotation, pieceX, ghostY, 2u);
|
|
draw_piece(piece, rotation, pieceX, pieceY, effect == 0xFFu ? 3u : 1u);
|
|
}
|
|
|
|
panel();
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Board mechanics: scoring, line clears, spawn, lock and moves */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static void update_best(void)
|
|
{
|
|
if (score > best) {
|
|
best = score;
|
|
bestDirty = true;
|
|
newBest = true;
|
|
}
|
|
}
|
|
|
|
static uint8_t clear_lines(void)
|
|
{
|
|
uint32_t full = 0;
|
|
uint8_t count = 0;
|
|
for (uint8_t y = 0; y < ROWS; y++) {
|
|
if (board[y] == 0xFFFFu) { full |= 1u << y; count++; }
|
|
}
|
|
if (!count)
|
|
return 0;
|
|
|
|
A->led(true);
|
|
for (uint8_t step = 1u; step <= COLS / 2u; step++) {
|
|
render(false, full, step);
|
|
A->blit_status(); A->blit_full(); A->delay_ms(28);
|
|
}
|
|
A->led(false);
|
|
|
|
int8_t dst = ROWS - 1;
|
|
for (int8_t src = ROWS - 1; src >= 0; src--) {
|
|
if (full & (1u << src))
|
|
continue;
|
|
if (dst != src)
|
|
board[dst] = board[src];
|
|
dst--;
|
|
}
|
|
while (dst >= 0) {
|
|
board[dst] = 0;
|
|
dst--;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static void spawn_piece(void)
|
|
{
|
|
piece = nextPiece;
|
|
nextPiece = take_piece();
|
|
rotation = 0;
|
|
pieceX = COLS / 2 - 2;
|
|
pieceY = HIDDEN_ROWS - 1;
|
|
if (collision(piece, rotation, pieceX, pieceY)) {
|
|
gameOver = true;
|
|
update_best();
|
|
}
|
|
}
|
|
|
|
static void lock_piece(void)
|
|
{
|
|
render(true, 0, 0xFFu);
|
|
A->blit_status(); A->blit_full(); A->delay_ms(45);
|
|
|
|
const uint16_t mask = MASK[piece][rotation];
|
|
bool above = false;
|
|
for (uint8_t i = 0; i < 16u; i++) {
|
|
if (!(mask & (uint16_t)(1u << i)))
|
|
continue;
|
|
const int x = pieceX + (i & 3u);
|
|
const int y = pieceY + (i >> 2);
|
|
if (y < HIDDEN_ROWS) above = true;
|
|
else board[y] |= (uint16_t)(1u << x);
|
|
}
|
|
if (above) {
|
|
gameOver = true;
|
|
update_best();
|
|
return;
|
|
}
|
|
|
|
const uint8_t cleared = clear_lines();
|
|
lines = (uint16_t)(lines + cleared);
|
|
while (level < 15u && lines >= (uint16_t)level * 10u)
|
|
level++;
|
|
score += (uint32_t)LINE_POINTS[cleared] * level;
|
|
update_best();
|
|
spawn_piece();
|
|
}
|
|
|
|
static bool move_down(bool manual)
|
|
{
|
|
if (!collision(piece, rotation, pieceX, pieceY + 1)) {
|
|
pieceY++;
|
|
if (manual) { score++; update_best(); }
|
|
return true;
|
|
}
|
|
lock_piece();
|
|
return false;
|
|
}
|
|
|
|
static void rotate_piece(void)
|
|
{
|
|
const uint8_t next = (uint8_t)((rotation + 1u) & 3u);
|
|
static const int8_t kick[5] = {0, -1, 1, -2, 2};
|
|
for (uint8_t i = 0; i < 5u; i++)
|
|
if (!collision(piece, next, pieceX + kick[i], pieceY)) {
|
|
pieceX += kick[i]; rotation = next; return;
|
|
}
|
|
if (!collision(piece, next, pieceX, pieceY - 1)) {
|
|
pieceY--; rotation = next;
|
|
}
|
|
}
|
|
|
|
static void hard_drop(void)
|
|
{
|
|
uint8_t distance = 0;
|
|
while (!collision(piece, rotation, pieceX, pieceY + 1)) {
|
|
pieceY++; distance++;
|
|
}
|
|
score += (uint32_t)distance * 2u;
|
|
update_best();
|
|
lock_piece();
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Input, screensaver handling and the main loop */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static void key_action(uint8_t key, bool repeat)
|
|
{
|
|
if (key == APP_KEY_EXIT) { running = false; return; }
|
|
if (gameOver) {
|
|
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
|
|
new_game();
|
|
return;
|
|
}
|
|
if (!repeat && key == APP_KEY_F) { paused = !paused; return; }
|
|
if (paused)
|
|
return;
|
|
|
|
int8_t direction = 0;
|
|
if (key == APP_KEY_4) direction = -1;
|
|
else if (key == APP_KEY_6) direction = 1;
|
|
else if (key == APP_KEY_UP || key == APP_KEY_DOWN) direction = A->nav_dir(key);
|
|
if (direction) {
|
|
if (!collision(piece, rotation, pieceX + direction, pieceY))
|
|
pieceX += direction;
|
|
return;
|
|
}
|
|
if (key == APP_KEY_8) { move_down(true); return; }
|
|
if (repeat)
|
|
return;
|
|
if (key == APP_KEY_MENU || key == APP_KEY_2) rotate_piece();
|
|
else if (key == APP_KEY_STAR || key == APP_KEY_0) hard_drop();
|
|
}
|
|
|
|
static void poll_key(void)
|
|
{
|
|
uint8_t key = A->get_key();
|
|
if (key == APP_KEY_SAVER) {
|
|
if (!paused && !gameOver) { paused = true; saverPaused = true; }
|
|
saverActive = true;
|
|
previousKey = APP_KEY_INVALID;
|
|
repeatMs = 0;
|
|
return;
|
|
}
|
|
if (key == APP_KEY_WAKE || key == APP_KEY_PTT) {
|
|
if (saverPaused) paused = false;
|
|
saverPaused = false;
|
|
saverActive = false;
|
|
key = APP_KEY_INVALID;
|
|
}
|
|
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;
|
|
}
|
|
|
|
static void new_game(void)
|
|
{
|
|
for (uint8_t y = 0; y < ROWS; y++)
|
|
board[y] = 0;
|
|
score = 0;
|
|
lines = 0;
|
|
level = 1;
|
|
gravityMs = 0;
|
|
paused = false;
|
|
gameOver = false;
|
|
saverPaused = false;
|
|
saverActive = false;
|
|
newBest = false;
|
|
bagPos = 7;
|
|
nextPiece = take_piece();
|
|
spawn_piece();
|
|
}
|
|
|
|
__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));
|
|
best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u;
|
|
bestDirty = false;
|
|
|
|
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best;
|
|
if (!randomState) randomState = 1;
|
|
previousKey = APP_KEY_INVALID;
|
|
repeatMs = 0;
|
|
running = true;
|
|
A->led(false);
|
|
A->backlight_on();
|
|
new_game();
|
|
|
|
while (running) {
|
|
poll_key();
|
|
if (!running)
|
|
break;
|
|
if (saverActive) {
|
|
A->backlight_update();
|
|
A->delay_ms(TICK_MS);
|
|
continue;
|
|
}
|
|
|
|
if (!paused && !gameOver) {
|
|
const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u);
|
|
gravityMs = (uint16_t)(gravityMs + TICK_MS);
|
|
if (gravityMs >= interval) {
|
|
gravityMs = 0;
|
|
move_down(false);
|
|
}
|
|
}
|
|
|
|
render(true, 0, 0);
|
|
A->blit_status();
|
|
A->blit_full();
|
|
A->backlight_update();
|
|
A->delay_ms(TICK_MS);
|
|
}
|
|
|
|
update_best();
|
|
if (bestDirty) {
|
|
cfg.magic = CFG_MAGIC;
|
|
cfg.version = 1u;
|
|
cfg.best = best;
|
|
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
|
|
}
|
|
A->led(false);
|
|
}
|