tdpro add minesweeper game

This commit is contained in:
pelgraine
2026-05-15 07:29:14 +10:00
parent cd2c8ae5b3
commit f6cc939c4d
5 changed files with 568 additions and 38 deletions
+79 -8
View File
@@ -31,6 +31,7 @@
#include "Tracescreen.h"
#include "GamesMenuScreen.h"
#include "SnakeScreen.h"
#include "MinesweeperScreen.h"
#ifdef MECK_WEB_READER
#include "WebReaderScreen.h"
#endif
@@ -704,6 +705,7 @@
#include "Tracescreen.h"
#include "GamesMenuScreen.h"
#include "SnakeScreen.h"
#include "MinesweeperScreen.h"
static TouchDrvGT911 gt911Touch;
static bool gt911Ready = false;
@@ -1229,6 +1231,11 @@ static void lastHeardToggleContact() {
return '\r';
}
// Minesweeper screen: tap = Enter (reveal cell / start / restart)
if (ui_task.isOnMinesweeperScreen()) {
return '\r';
}
// Home screen FIRST page: tile taps (virtual coordinate hit test)
if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) {
const int tileW = 40, tileH = 22, gapX = 1, gapY = 1;
@@ -1502,6 +1509,15 @@ static void lastHeardToggleContact() {
}
}
// Minesweeper screen: swipes move cursor
if (ui_task.isOnMinesweeperScreen()) {
if (horizontal) {
return (dx < 0) ? 'a' : 'd';
} else {
return (dy < 0) ? 'w' : 's';
}
}
// Games menu: vertical swipe scrolls list
if (ui_task.isOnGamesMenu()) {
if (!horizontal) {
@@ -1587,6 +1603,11 @@ static void lastHeardToggleContact() {
return 'q';
}
// Minesweeper screen: long press toggles flag on cursor cell
if (ui_task.isOnMinesweeperScreen()) {
return 'f';
}
// Games menu: long press exits to home
if (ui_task.isOnGamesMenu()) {
return 'q';
@@ -3432,6 +3453,13 @@ void loop() {
ui_task.gotoGamesMenu();
}
}
// Minesweeper screen: check if Exit was triggered
if (ui_task.isOnMinesweeperScreen()) {
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
ui_task.gotoGamesMenu();
}
}
// Games menu: check if game launch was triggered
if (ui_task.isOnGamesMenu()) {
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
@@ -3440,6 +3468,7 @@ void loop() {
gm->clearFlags();
switch (sel) {
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
default: break;
}
}
@@ -3484,6 +3513,13 @@ void loop() {
ui_task.gotoGamesMenu();
}
}
// Minesweeper screen: check if Exit was triggered
if (ui_task.isOnMinesweeperScreen()) {
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
ui_task.gotoGamesMenu();
}
}
// Games menu: check if game launch was triggered
if (ui_task.isOnGamesMenu()) {
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
@@ -3492,6 +3528,7 @@ void loop() {
gm->clearFlags();
switch (sel) {
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
default: break;
}
}
@@ -3696,6 +3733,12 @@ void loop() {
if (ss && ss->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnMinesweeperScreen()) {
ui_task.injectKey('q');
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnGamesMenu()) {
ui_task.gotoHomeScreen();
} else if (ui_task.isOnChannelPickerScreen()) {
@@ -3862,6 +3905,7 @@ void loop() {
gm->clearFlags();
switch (sel) {
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
default: break;
}
}
@@ -3872,6 +3916,13 @@ void loop() {
if (ss && ss->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnMinesweeperScreen()) {
// Minesweeper: Enter reveals cell or starts/restarts
ui_task.injectKey('\r');
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnChannelPickerScreen()) {
// Channel picker: Enter selects channel
ui_task.injectKey('\r');
@@ -4978,7 +5029,7 @@ void handleKeyboardInput() {
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -4997,7 +5048,7 @@ void handleKeyboardInput() {
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -5020,7 +5071,7 @@ void handleKeyboardInput() {
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -5039,7 +5090,7 @@ void handleKeyboardInput() {
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_WEB_READER
|| ui_task.isOnWebReader()
#endif
@@ -5062,7 +5113,7 @@ void handleKeyboardInput() {
} else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnSnakeScreen()
|| ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
@@ -5081,7 +5132,7 @@ void handleKeyboardInput() {
} else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|| ui_task.isOnTraceScreen()
|| ui_task.isOnSnakeScreen()
|| ui_task.isOnSnakeScreen() || ui_task.isOnMinesweeperScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
@@ -5118,7 +5169,7 @@ void handleKeyboardInput() {
gm->clearFlags();
switch (sel) {
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
// case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
// case GAME_2048: ui_task.goto2048Screen(); break;
default: break;
}
@@ -5129,6 +5180,12 @@ void handleKeyboardInput() {
if (ss && ss->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnMinesweeperScreen()) {
ui_task.injectKey('\r');
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
ui_task.gotoGamesMenu();
}
} else if (ui_task.isOnChannelPickerScreen()) {
ui_task.injectKey('\r'); // Picker handles Enter: selects channel + sets wantsExit
ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen();
@@ -5308,8 +5365,12 @@ void handleKeyboardInput() {
break;
case 'f':
// Minesweeper: F toggles flag on cursor cell
if (ui_task.isOnMinesweeperScreen()) {
ui_task.injectKey('f');
}
// Start discovery scan from home/contacts screen, or rescan on discovery screen
if (ui_task.isOnContactsScreen() || ui_task.isOnHomeScreen()) {
else if (ui_task.isOnContactsScreen() || ui_task.isOnHomeScreen()) {
Serial.println("Starting discovery scan...");
the_mesh.startDiscovery();
ui_task.gotoDiscoveryScreen();
@@ -5423,6 +5484,16 @@ void handleKeyboardInput() {
}
break;
}
// Minesweeper screen: Q goes back to games menu
if (ui_task.isOnMinesweeperScreen()) {
ui_task.injectKey('q');
MinesweeperScreen* ms = (MinesweeperScreen*)ui_task.getMinesweeperScreen();
if (ms && ms->wantsExit()) {
Serial.println("Nav: Minesweeper -> Games Menu");
ui_task.gotoGamesMenu();
}
break;
}
// Games menu: Q goes back to home
if (ui_task.isOnGamesMenu()) {
Serial.println("Nav: Games Menu -> Home");
@@ -18,7 +18,7 @@ class UITask;
enum GameID {
GAME_NONE = 0,
GAME_SNAKE,
// GAME_MINESWEEPER,
GAME_MINESWEEPER,
// GAME_2048,
GAME_COUNT // Must be last -- used for array sizing
};
@@ -38,12 +38,12 @@ private:
const char* description;
};
static constexpr int NUM_GAMES = 1; // Increment as games are added
static constexpr int NUM_GAMES = 2; // Increment as games are added
static const GameEntry* getGames() {
static const GameEntry games[NUM_GAMES] = {
{ GAME_SNAKE, "Snake", "Classic Nokia-style" },
// { GAME_MINESWEEPER, "Minesweeper", "Tap to reveal" },
{ GAME_MINESWEEPER, "Minesweeper", "Find the mines" },
// { GAME_2048, "2048", "Slide and merge" },
};
return games;
@@ -110,11 +110,7 @@ public:
// --- Game list ---
int y = 18;
#if defined(LilyGo_T5S3_EPaper_Pro)
int lineH = 16;
#else
int lineH = 20;
#endif
for (int i = 0; i < NUM_GAMES; i++) {
bool selected = (i == _cursor);
@@ -129,26 +125,10 @@ public:
}
#if defined(LilyGo_T5S3_EPaper_Pro)
// T5S3: name centred, description below
display.drawTextCentered(display.width() / 2, y + 1, getGames()[i].name);
if (!selected) display.setColor(DisplayDriver::GREEN);
// Description on next line at smaller size
display.setTextSize(0);
display.drawTextCentered(display.width() / 2, y + 9, getGames()[i].description);
display.setTextSize(1);
lineH = 20; // Accommodate two-line entries
display.drawTextCentered(display.width() / 2, y + 2, getGames()[i].name);
#else
// T-Deck Pro: name + description on one line
char buf[48];
snprintf(buf, sizeof(buf), "%s", getGames()[i].name);
display.setCursor(6, y + 2);
display.print(buf);
// Description in green (or dark if highlighted)
if (!selected) display.setColor(DisplayDriver::GREEN);
int nameW = display.getTextWidth(buf);
display.setCursor(6 + nameW + 8, y + 2);
display.print(getGames()[i].description);
display.print(getGames()[i].name);
#endif
y += lineH;
@@ -174,11 +154,7 @@ public:
// --- T5S3 touch: tap to select game entry ---
int selectRowAtVY(int vy) {
int y = 18;
#if defined(LilyGo_T5S3_EPaper_Pro)
int lineH = 20;
#else
int lineH = 20;
#endif
int lineH = 16;
if (vy < y) return 0; // Above list
int row = (vy - y) / lineH;
if (row >= NUM_GAMES) return 0; // Below list
@@ -0,0 +1,466 @@
#pragma once
// =============================================================================
// MinesweeperScreen -- Classic Minesweeper for Meck e-ink devices
//
// 9x9 grid, 10 mines (classic Beginner difficulty).
// First reveal is always safe -- mines are placed after the first click.
// Fully turn-based: no tick timer, renders only on input. Perfect for e-ink.
//
// T-Deck Pro: 14x14 pixel cells (126x126 grid area on 240x320 display)
// T5S3: 8x8 pixel cells (72x72 grid area on 128x128 virtual display)
// =============================================================================
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
// Forward declarations
class UITask;
// -- Grid parameters --
#define MINE_GRID_W 9
#define MINE_GRID_H 9
#define MINE_COUNT 10
#define MINE_TOTAL (MINE_GRID_W * MINE_GRID_H)
// -- Cell sizes per platform --
#if defined(LilyGo_T5S3_EPaper_Pro)
#define MINE_CELL 8
#define MINE_HDR 14
#define MINE_FTR 10
#else
#define MINE_CELL 14
#define MINE_HDR 14
#define MINE_FTR 14
#endif
#define MINE_VALUE 9 // Content value indicating a mine
class MinesweeperScreen : public UIScreen {
public:
enum GameState { READY, PLAYING, WON, LOST };
enum CellState : uint8_t { CELL_HIDDEN, CELL_REVEALED, CELL_FLAGGED };
private:
UITask* _task;
bool _wantsExit;
// Grid
uint8_t _content[MINE_TOTAL]; // 0-8 = adjacent mine count, MINE_VALUE = mine
CellState _cellState[MINE_TOTAL]; // Hidden, revealed, or flagged
// Game state
GameState _state;
bool _minesPlaced; // False until first reveal (first-click safety)
int _cursorX, _cursorY;
int _flagCount;
int _revealedCount;
// Display layout (recomputed each render based on game state)
int _offsetX, _offsetY;
bool _cursorBlink; // Toggles each render for slow blink cursor
// Simple xorshift PRNG
uint16_t _rngState;
uint16_t rng() {
_rngState ^= _rngState << 7;
_rngState ^= _rngState >> 9;
_rngState ^= _rngState << 8;
return _rngState;
}
// -- Grid helpers --
int idx(int x, int y) const { return y * MINE_GRID_W + x; }
bool inBounds(int x, int y) const { return x >= 0 && x < MINE_GRID_W && y >= 0 && y < MINE_GRID_H; }
void resetGrid() {
memset(_content, 0, sizeof(_content));
for (int i = 0; i < MINE_TOTAL; i++) _cellState[i] = CELL_HIDDEN;
_minesPlaced = false;
_flagCount = 0;
_revealedCount = 0;
_cursorX = MINE_GRID_W / 2;
_cursorY = MINE_GRID_H / 2;
}
// Place mines randomly, excluding the first-clicked cell and its neighbours
void placeMines(int safeX, int safeY) {
_rngState = (uint16_t)(millis() ^ 0xC0DE);
int placed = 0;
while (placed < MINE_COUNT) {
int x = rng() % MINE_GRID_W;
int y = rng() % MINE_GRID_H;
// Skip safe zone (first click + 8 neighbours)
if (abs(x - safeX) <= 1 && abs(y - safeY) <= 1) continue;
// Skip if already a mine
if (_content[idx(x, y)] == MINE_VALUE) continue;
_content[idx(x, y)] = MINE_VALUE;
placed++;
}
// Compute adjacency counts
for (int cy = 0; cy < MINE_GRID_H; cy++) {
for (int cx = 0; cx < MINE_GRID_W; cx++) {
if (_content[idx(cx, cy)] == MINE_VALUE) continue;
int count = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
int nx = cx + dx, ny = cy + dy;
if (inBounds(nx, ny) && _content[idx(nx, ny)] == MINE_VALUE) count++;
}
}
_content[idx(cx, cy)] = count;
}
}
_minesPlaced = true;
}
// Flood-fill reveal from (x,y). Reveals empty cells and their numbered borders.
void floodReveal(int x, int y) {
if (!inBounds(x, y)) return;
int i = idx(x, y);
if (_cellState[i] != CELL_HIDDEN) return;
if (_content[i] == MINE_VALUE) return;
_cellState[i] = CELL_REVEALED;
_revealedCount++;
// If this cell is 0 (no adjacent mines), reveal all neighbours
if (_content[i] == 0) {
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
floodReveal(x + dx, y + dy);
}
}
}
}
// Reveal a cell. Returns false if mine was hit.
bool revealCell(int x, int y) {
int i = idx(x, y);
if (_cellState[i] != CELL_HIDDEN) return true; // Already revealed or flagged
// First click: place mines safely
if (!_minesPlaced) {
placeMines(x, y);
}
if (_content[i] == MINE_VALUE) {
// Hit a mine -- game over
_cellState[i] = CELL_REVEALED;
_state = LOST;
revealAllMines();
return false;
}
floodReveal(x, y);
checkWin();
return true;
}
void toggleFlag(int x, int y) {
int i = idx(x, y);
if (_cellState[i] == CELL_HIDDEN) {
_cellState[i] = CELL_FLAGGED;
_flagCount++;
} else if (_cellState[i] == CELL_FLAGGED) {
_cellState[i] = CELL_HIDDEN;
_flagCount--;
}
// Can't flag revealed cells
}
void checkWin() {
// Win when all non-mine cells are revealed
if (_revealedCount == MINE_TOTAL - MINE_COUNT) {
_state = WON;
// Auto-flag remaining mines
for (int i = 0; i < MINE_TOTAL; i++) {
if (_content[i] == MINE_VALUE && _cellState[i] == CELL_HIDDEN) {
_cellState[i] = CELL_FLAGGED;
_flagCount++;
}
}
}
}
void revealAllMines() {
for (int i = 0; i < MINE_TOTAL; i++) {
if (_content[i] == MINE_VALUE) {
_cellState[i] = CELL_REVEALED;
}
}
}
// -- Drawing helpers --
void drawCell(DisplayDriver& display, int gx, int gy) const {
int px = _offsetX + gx * MINE_CELL;
int py = _offsetY + gy * MINE_CELL;
int i = idx(gx, gy);
bool isCursor = (gx == _cursorX && gy == _cursorY && _state == PLAYING);
if (_cellState[i] == CELL_HIDDEN) {
if (isCursor && !_cursorBlink) {
// Cursor blink OFF phase: outline only (visible gap in the solid grid)
display.setColor(DisplayDriver::LIGHT);
display.drawRect(px, py, MINE_CELL, MINE_CELL);
display.drawRect(px + 1, py + 1, MINE_CELL - 2, MINE_CELL - 2);
} else {
// Solid filled with 1px inset (preserves grid lines between cells)
display.setColor(DisplayDriver::LIGHT);
display.fillRect(px + 1, py + 1, MINE_CELL - 2, MINE_CELL - 2);
}
} else if (_cellState[i] == CELL_FLAGGED) {
if (isCursor && !_cursorBlink) {
// Cursor blink OFF phase: outline with F
display.setColor(DisplayDriver::LIGHT);
display.drawRect(px, py, MINE_CELL, MINE_CELL);
display.drawRect(px + 1, py + 1, MINE_CELL - 2, MINE_CELL - 2);
} else {
// Solid fill with 1px inset
display.setColor(DisplayDriver::LIGHT);
display.fillRect(px + 1, py + 1, MINE_CELL - 2, MINE_CELL - 2);
}
// F overlay
display.setColor((isCursor && !_cursorBlink) ? DisplayDriver::LIGHT : DisplayDriver::DARK);
#if defined(LilyGo_T5S3_EPaper_Pro)
display.setTextSize(0);
display.drawTextCentered(px + MINE_CELL / 2, py + 1, "F");
#else
display.setTextSize(1);
display.drawTextCentered(px + MINE_CELL / 2, py + 3, "F");
#endif
} else {
// Revealed cell: thin border
display.setColor(DisplayDriver::LIGHT);
display.drawRect(px, py, MINE_CELL, MINE_CELL);
if (_content[i] == MINE_VALUE) {
// Mine: solid dot in centre
int dotR = MINE_CELL / 4;
if (dotR < 2) dotR = 2;
display.setColor(DisplayDriver::LIGHT);
display.fillRect(px + MINE_CELL/2 - dotR, py + MINE_CELL/2 - dotR, dotR*2, dotR*2);
} else if (_content[i] > 0) {
// Number 1-8
char num[2] = { (char)('0' + _content[i]), '\0' };
display.setColor(DisplayDriver::GREEN);
#if defined(LilyGo_T5S3_EPaper_Pro)
display.setTextSize(0);
display.drawTextCentered(px + MINE_CELL / 2, py + 1, num);
#else
display.setTextSize(1);
display.drawTextCentered(px + MINE_CELL / 2, py + 3, num);
#endif
}
// Revealed 0: just the border (already drawn)
// Cursor on revealed cell: green double border
if (isCursor) {
display.setColor(DisplayDriver::GREEN);
display.drawRect(px, py, MINE_CELL, MINE_CELL);
display.drawRect(px + 1, py + 1, MINE_CELL - 2, MINE_CELL - 2);
}
}
}
public:
MinesweeperScreen(UITask* task)
: _task(task), _wantsExit(false), _state(READY), _minesPlaced(false),
_cursorX(MINE_GRID_W / 2), _cursorY(MINE_GRID_H / 2),
_flagCount(0), _revealedCount(0),
_offsetX(0), _offsetY(0), _cursorBlink(false), _rngState(0xBEEF) {
memset(_content, 0, sizeof(_content));
for (int i = 0; i < MINE_TOTAL; i++) _cellState[i] = CELL_HIDDEN;
}
bool wantsExit() const { return _wantsExit; }
void clearExit() { _wantsExit = false; }
GameState getState() const { return _state; }
void enter() {
_wantsExit = false;
// If game was PLAYING, resume where we left off
// If READY, WON, or LOST, show that state as-is
}
// ------- Input -------
bool handleInput(char c) override {
switch (_state) {
case READY:
if (c == '\r') {
resetGrid();
_state = PLAYING;
return true;
}
if (c == 'q' || c == 'Q') { _wantsExit = true; return true; }
return false;
case PLAYING:
switch (c) {
case 'w': case 'W': if (_cursorY > 0) _cursorY--; return true;
case 's': case 'S': if (_cursorY < MINE_GRID_H - 1) _cursorY++; return true;
case 'a': case 'A': if (_cursorX > 0) _cursorX--; return true;
case 'd': case 'D': if (_cursorX < MINE_GRID_W - 1) _cursorX++; return true;
case '\r':
revealCell(_cursorX, _cursorY);
return true;
case 'f': case 'F':
toggleFlag(_cursorX, _cursorY);
return true;
case 'q': case 'Q':
_wantsExit = true;
return true;
default: return false;
}
case WON:
case LOST:
if (c == '\r') {
resetGrid();
_state = PLAYING;
return true;
}
if (c == 'q' || c == 'Q') { _wantsExit = true; return true; }
return false;
}
return false;
}
// ------- Render -------
int render(DisplayDriver& display) override {
// Compute grid offset based on state
// PLAYING: full screen (no header/footer) for clean grid
// Other states: header + footer visible
int gridPixW = MINE_GRID_W * MINE_CELL;
int gridPixH = MINE_GRID_H * MINE_CELL;
if (_state == PLAYING) {
_offsetX = (display.width() - gridPixW) / 2;
_offsetY = (display.height() - gridPixH) / 2;
} else {
int usableH = display.height() - MINE_HDR - MINE_FTR;
_offsetX = (display.width() - gridPixW) / 2;
_offsetY = MINE_HDR + (usableH - gridPixH) / 2;
}
// Toggle cursor blink each render cycle
if (_state == PLAYING) _cursorBlink = !_cursorBlink;
display.startFrame();
display.setTextSize(1);
if (_state == READY) {
// --- READY: header + instructions + footer ---
display.setColor(DisplayDriver::GREEN);
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(display.width() / 2, 2, "Minesweeper");
#else
display.setCursor(2, 2);
display.print("Minesweeper");
#endif
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, MINE_HDR - 2, display.width(), 1);
int cx = display.width() / 2;
int y = MINE_HDR + 10;
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(cx, y, "Minesweeper");
y += 16;
display.setColor(DisplayDriver::GREEN);
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(cx, y, "Swipe to move cursor");
y += 11;
display.drawTextCentered(cx, y, "Tap to reveal");
y += 11;
display.drawTextCentered(cx, y, "Long press to flag");
#else
display.drawTextCentered(cx, y, "W/S/A/D to move cursor");
y += 11;
display.drawTextCentered(cx, y, "Enter to reveal a cell");
y += 11;
display.drawTextCentered(cx, y, "F to flag a mine");
#endif
y += 16;
display.setColor(DisplayDriver::LIGHT);
char info[32];
snprintf(info, sizeof(info), "%dx%d grid, %d mines", MINE_GRID_W, MINE_GRID_H, MINE_COUNT);
display.drawTextCentered(cx, y, info);
y += 16;
#if defined(LilyGo_T5S3_EPaper_Pro)
display.drawTextCentered(cx, y, "Tap to start");
#else
display.drawTextCentered(cx, y, "Press Enter to start");
#endif
// Footer
display.setColor(DisplayDriver::LIGHT);
#if !defined(LilyGo_T5S3_EPaper_Pro)
int fy = display.height() - 12;
display.drawRect(0, fy - 2, display.width(), 1);
display.setCursor(2, fy);
display.print("Enter:Start Q:Back");
#endif
return 5000;
} else if (_state == PLAYING) {
// --- PLAYING: full screen grid, no header/footer ---
// Draw grid border
display.setColor(DisplayDriver::LIGHT);
display.drawRect(_offsetX - 1, _offsetY - 1, gridPixW + 2, gridPixH + 2);
// Draw all cells
for (int gy = 0; gy < MINE_GRID_H; gy++) {
for (int gx = 0; gx < MINE_GRID_W; gx++) {
drawCell(display, gx, gy);
}
}
return 100; // Blink cycle -- clamped to 800ms by render floor
} else {
// --- WON / LOST: grid + overlay ---
// Draw grid border
display.setColor(DisplayDriver::LIGHT);
display.drawRect(_offsetX - 1, _offsetY - 1, gridPixW + 2, gridPixH + 2);
// Draw all cells (no cursor blink in end state)
for (int gy = 0; gy < MINE_GRID_H; gy++) {
for (int gx = 0; gx < MINE_GRID_W; gx++) {
drawCell(display, gx, gy);
}
}
// Overlay
int cx = display.width() / 2;
int cy = display.height() / 2;
int boxW = display.width() * 3 / 4;
int boxH = 50;
int boxX = cx - boxW / 2;
int boxY = cy - boxH / 2;
display.setColor(DisplayDriver::DARK);
display.fillRect(boxX, boxY, boxW, boxH);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(boxX, boxY, boxW, boxH);
int ty = boxY + 10;
if (_state == WON) {
display.setColor(DisplayDriver::YELLOW);
display.drawTextCentered(cx, ty, "Cleared!");
} else {
display.drawTextCentered(cx, ty, "Boom!");
}
ty += 16;
display.setColor(DisplayDriver::GREEN);
display.drawTextCentered(cx, ty, "Enter:Retry Q:Back");
return 5000;
}
}
};
@@ -11,6 +11,7 @@
#include "Tracescreen.h"
#include "GamesMenuScreen.h"
#include "SnakeScreen.h"
#include "MinesweeperScreen.h"
#ifdef MECK_WEB_READER
#include "WebReaderScreen.h"
#endif
@@ -1403,6 +1404,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
trace_screen = new TraceScreen(this, &rtc_clock);
games_menu_screen = new GamesMenuScreen(this);
snake_screen = new SnakeScreen(this, &rtc_clock);
minesweeper_screen = new MinesweeperScreen(this);
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
lock_screen = new LockScreen(this, &rtc_clock, node_prefs);
#endif
@@ -3145,6 +3147,17 @@ void UITask::gotoSnakeScreen() {
_next_refresh = 100;
}
void UITask::gotoMinesweeperScreen() {
MinesweeperScreen* ms = (MinesweeperScreen*)minesweeper_screen;
ms->enter();
setCurrScreen(minesweeper_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
void UITask::onTraceResult(uint32_t tag, uint8_t flags, const uint8_t* path_snrs,
const uint8_t* path_hashes, uint8_t path_len, int8_t final_snr) {
TraceScreen* ts = (TraceScreen*)trace_screen;
+4
View File
@@ -102,6 +102,7 @@ class UITask : public AbstractUITask {
UIScreen* trace_screen; // Trace path screen (standalone trace tool)
UIScreen* games_menu_screen; // Games launcher menu
UIScreen* snake_screen; // Snake game screen
UIScreen* minesweeper_screen; // Minesweeper game screen
#ifdef MECK_WEB_READER
UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required)
#endif
@@ -206,6 +207,7 @@ public:
void gotoTraceScreen(); // Navigate to trace path screen
void gotoGamesMenu(); // Navigate to games launcher menu
void gotoSnakeScreen(); // Navigate to snake game
void gotoMinesweeperScreen(); // Navigate to minesweeper game
#if HAS_GPS
void gotoMapScreen(); // Navigate to map tile screen
#endif
@@ -265,6 +267,7 @@ public:
bool isOnTraceScreen() const { return curr == trace_screen; }
bool isOnGamesMenu() const { return curr == games_menu_screen; }
bool isOnSnakeScreen() const { return curr == snake_screen; }
bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; }
bool isOnMapScreen() const { return curr == map_screen; }
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
bool isLocked() const { return _locked; }
@@ -352,6 +355,7 @@ public:
UIScreen* getTraceScreen() const { return trace_screen; }
UIScreen* getGamesMenuScreen() const { return games_menu_screen; }
UIScreen* getSnakeScreen() const { return snake_screen; }
UIScreen* getMinesweeperScreen() const { return minesweeper_screen; }
UIScreen* getMapScreen() const { return map_screen; }
#ifdef MECK_WEB_READER
UIScreen* getWebReaderScreen() const { return web_reader; }