mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-17 06:45:50 +02:00
tdpro add minesweeper game
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user