Initial (and hopefully last) commit
This commit is contained in:
commit
dea8e8be68
41 changed files with 126867 additions and 0 deletions
5
code/.gitignore
vendored
Normal file
5
code/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
10
code/.vscode/extensions.json
vendored
Normal file
10
code/.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
37
code/include/README
Normal file
37
code/include/README
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the convention is to give header files names that end with `.h'.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
code/lib/README
Normal file
46
code/lib/README
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
16
code/platformio.ini
Normal file
16
code/platformio.ini
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp32-c3]
|
||||
platform = espressif32
|
||||
board = esp32-c3-devkitm-1
|
||||
build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1
|
||||
framework = arduino
|
||||
lib_deps = olikraus/U8g2@^2.36.18
|
||||
900
code/src/main.cpp
Normal file
900
code/src/main.cpp
Normal file
|
|
@ -0,0 +1,900 @@
|
|||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <U8g2lib.h>
|
||||
#include <Preferences.h>
|
||||
#include <cstring>
|
||||
|
||||
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 9, /* data=*/ 8);
|
||||
Preferences preferences;
|
||||
|
||||
#define BTNU 0
|
||||
#define BTNR 1
|
||||
#define BTND 3
|
||||
#define BTNL 10
|
||||
#define BUZZER 7
|
||||
const uint8_t ledcChannel = 0;
|
||||
const uint16_t SCREEN_W = 128;
|
||||
const uint16_t SCREEN_H = 64;
|
||||
const uint8_t HUD_H = 8;
|
||||
const uint8_t CELL = 4;
|
||||
const uint8_t GRID_W = SCREEN_W / CELL;
|
||||
const uint8_t GRID_H = (SCREEN_H - HUD_H) / CELL;
|
||||
const uint16_t MAX_SNAKE = GRID_W * GRID_H;
|
||||
|
||||
const uint16_t BASE_STEP_MS = 170;
|
||||
const uint16_t MIN_STEP_MS = 85;
|
||||
const uint16_t SPEEDUP_PER_FOOD_MS = 5;
|
||||
const uint16_t SPRINT_STEP_MS = 50;
|
||||
const uint8_t SPRINT_HOLD_FRAMES = 5;
|
||||
|
||||
const uint16_t MIN_TONE_FREQ = 500;
|
||||
const uint16_t MAX_TONE_FREQ = 2000;
|
||||
const uint16_t MENU_DEBOUNCE_MS = 150;
|
||||
const uint16_t NAME_ENTRY_DWELL_MS = 350;
|
||||
const uint8_t LEADERBOARD_SIZE = 5;
|
||||
const uint8_t NAME_LENGTH = 8;
|
||||
const uint8_t LEADERBOARD_BOARDS = 3; // Easy, Medium, Hard
|
||||
|
||||
// Difficulty presets and custom bounds
|
||||
const uint16_t DIFF_EASY_MS = 200;
|
||||
const uint16_t DIFF_MEDIUM_MS = BASE_STEP_MS;
|
||||
const uint16_t DIFF_HARD_MS = 130;
|
||||
const uint16_t CUSTOM_MIN_MS = 85;
|
||||
const uint16_t CUSTOM_MAX_MS = 300;
|
||||
// Difficulty offsets and preset speedup-per-food values
|
||||
const int16_t DIFF_OFFSET_EASY = 30;
|
||||
const int16_t DIFF_OFFSET_MED = 0;
|
||||
const int16_t DIFF_OFFSET_HARD = -40;
|
||||
const uint16_t DIFF_SPEEDUP_EASY = 3;
|
||||
const uint16_t DIFF_SPEEDUP_MED = SPEEDUP_PER_FOOD_MS;
|
||||
const uint16_t DIFF_SPEEDUP_HARD = 7;
|
||||
|
||||
struct Cell {
|
||||
int8_t x;
|
||||
int8_t y;
|
||||
};
|
||||
|
||||
enum Direction : uint8_t { UP, RIGHT, DOWN, LEFT };
|
||||
enum GameState : uint8_t { START, PLAYING, GAME_OVER, SETTINGS, LEADERBOARD, NAME_ENTRY };
|
||||
|
||||
struct LeaderboardEntry {
|
||||
char name[NAME_LENGTH + 1];
|
||||
uint16_t score;
|
||||
};
|
||||
|
||||
GameState gameState = START;
|
||||
// toneFreq == 0 means buzzer OFF; otherwise valid range is 500..2000 in 100 Hz steps
|
||||
uint16_t toneFreq = 1200;
|
||||
bool isSprinting = false;
|
||||
uint8_t sprintHoldU = 0, sprintHoldR = 0, sprintHoldD = 0, sprintHoldL = 0;
|
||||
// settingsMenuIndex removed; using settingsScrollOffset and dynamic list
|
||||
uint32_t lastSettingsInputMs = 0;
|
||||
// Settings UI state: scroll offset, total items. Selector stays in middle of visible window.
|
||||
uint8_t settingsScrollOffset = 0;
|
||||
const uint8_t SETTINGS_VISIBLE = 3; // visible rows
|
||||
|
||||
// Difficulty selection
|
||||
uint8_t difficultyPreset = 1; // 0=Easy,1=Medium,2=Hard,3=Custom
|
||||
uint16_t customStepMs = BASE_STEP_MS;
|
||||
// Sprint enable/disable (settings)
|
||||
bool sprintAllowed = true;
|
||||
// Custom advanced params
|
||||
uint16_t customSpeedupPerFood = SPEEDUP_PER_FOOD_MS;
|
||||
uint16_t customMinStepMs = MIN_STEP_MS;
|
||||
uint16_t customMaxStepMs = DIFF_MEDIUM_MS * 2;
|
||||
|
||||
// Calculate dynamic settings total depending on whether custom step row is present
|
||||
uint8_t settingsTotal() {
|
||||
// If custom difficulty selected, include extra rows: custom start, speedup, min, max
|
||||
if (difficultyPreset == 3) return 10; // blank, buzzer, sprint, difficulty, customStart, customSpeedup, customMin, customMax, back, blank
|
||||
return 6; // blank, buzzer, sprint, difficulty, back, blank
|
||||
}
|
||||
|
||||
uint8_t leaderboardBoardIndexForDifficulty(uint8_t difficulty) {
|
||||
return (difficulty < LEADERBOARD_BOARDS) ? difficulty : 1;
|
||||
}
|
||||
|
||||
uint8_t activeLeaderboardBoardIndex() {
|
||||
return leaderboardBoardIndexForDifficulty(difficultyPreset);
|
||||
}
|
||||
|
||||
// NAME_ENTRY: require L+R release before next select
|
||||
bool lrComboReleased = true;
|
||||
LeaderboardEntry leaderboard[LEADERBOARD_BOARDS][LEADERBOARD_SIZE];
|
||||
char pendingName[NAME_LENGTH + 1] = {0};
|
||||
uint16_t pendingScore = 0;
|
||||
uint8_t pendingLeaderboardBoard = 0;
|
||||
uint8_t keyboardRow = 0;
|
||||
uint8_t keyboardCol = 0;
|
||||
Cell snake[MAX_SNAKE];
|
||||
uint16_t snakeLen = 0;
|
||||
Cell food = {0, 0};
|
||||
Direction currentDir = RIGHT;
|
||||
Direction nextDir = RIGHT;
|
||||
uint16_t score = 0;
|
||||
uint16_t highScore = 0;
|
||||
uint32_t lastStepMs = 0;
|
||||
uint32_t stateChangeMs = 0;
|
||||
uint32_t inputIgnoreUntilMs = 0; // ignore button input until this millis() (used after name-entry OK)
|
||||
|
||||
void resetGame();
|
||||
|
||||
bool cellEquals(const Cell &first, const Cell &second) {
|
||||
return first.x == second.x && first.y == second.y;
|
||||
}
|
||||
|
||||
bool isSnakeCell(const Cell &target) {
|
||||
for (uint16_t index = 0; index < snakeLen; ++index) {
|
||||
if (cellEquals(snake[index], target)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void initLeaderboardDefaults() {
|
||||
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
|
||||
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
|
||||
strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
|
||||
leaderboard[board][index].name[NAME_LENGTH] = '\0';
|
||||
leaderboard[board][index].score = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadLeaderboardBoard(uint8_t board) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "leader%u", board);
|
||||
const size_t loadedBytes = preferences.getBytes(key, leaderboard[board], sizeof(leaderboard[board]));
|
||||
if (loadedBytes != sizeof(leaderboard[board])) {
|
||||
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
|
||||
strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
|
||||
leaderboard[board][index].name[NAME_LENGTH] = '\0';
|
||||
leaderboard[board][index].score = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void saveLeaderboardBoard(uint8_t board) {
|
||||
char key[16];
|
||||
snprintf(key, sizeof(key), "leader%u", board);
|
||||
preferences.putBytes(key, leaderboard[board], sizeof(leaderboard[board]));
|
||||
}
|
||||
|
||||
void loadHighScore() {
|
||||
preferences.begin("snake", true);
|
||||
const bool buzzerEnabledLegacy = preferences.getBool("buzzerEnabled", true);
|
||||
toneFreq = preferences.getUShort("toneFreq", 1200);
|
||||
difficultyPreset = preferences.getUShort("diffPreset", 1);
|
||||
customStepMs = preferences.getUShort("customStepMs", BASE_STEP_MS);
|
||||
customSpeedupPerFood = preferences.getUShort("customSpeedup", SPEEDUP_PER_FOOD_MS);
|
||||
customMinStepMs = preferences.getUShort("customMin", MIN_STEP_MS);
|
||||
customMaxStepMs = preferences.getUShort("customMax", DIFF_MEDIUM_MS * 2);
|
||||
sprintAllowed = preferences.getBool("sprintAllowed", true);
|
||||
initLeaderboardDefaults();
|
||||
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
|
||||
loadLeaderboardBoard(board);
|
||||
}
|
||||
if (toneFreq != 0 && (toneFreq < MIN_TONE_FREQ || toneFreq > MAX_TONE_FREQ || ((toneFreq - MIN_TONE_FREQ) % 100) != 0)) {
|
||||
toneFreq = 1200;
|
||||
}
|
||||
if (!buzzerEnabledLegacy) {
|
||||
toneFreq = 0;
|
||||
}
|
||||
highScore = (difficultyPreset < 3) ? leaderboard[activeLeaderboardBoardIndex()][0].score : 0;
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void saveHighScore() {
|
||||
preferences.begin("snake", false);
|
||||
preferences.putUShort("toneFreq", toneFreq);
|
||||
preferences.putUShort("diffPreset", difficultyPreset);
|
||||
preferences.putUShort("customStepMs", customStepMs);
|
||||
preferences.putUShort("customSpeedup", customSpeedupPerFood);
|
||||
preferences.putUShort("customMin", customMinStepMs);
|
||||
preferences.putUShort("customMax", customMaxStepMs);
|
||||
preferences.putBool("sprintAllowed", sprintAllowed);
|
||||
for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
|
||||
saveLeaderboardBoard(board);
|
||||
}
|
||||
if (difficultyPreset < 3) {
|
||||
preferences.putUShort("highScore", leaderboard[activeLeaderboardBoardIndex()][0].score);
|
||||
} else {
|
||||
preferences.putUShort("highScore", 0);
|
||||
}
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
bool qualifiesForLeaderboard(uint16_t scoreValue) {
|
||||
if (difficultyPreset >= 3) return false;
|
||||
const uint8_t board = activeLeaderboardBoardIndex();
|
||||
return scoreValue > leaderboard[board][LEADERBOARD_SIZE - 1].score ||
|
||||
leaderboard[board][LEADERBOARD_SIZE - 1].name[0] == '\0' ||
|
||||
leaderboard[board][LEADERBOARD_SIZE - 1].score == 0;
|
||||
}
|
||||
|
||||
void syncHighScoreFromLeaderboard(uint8_t board) {
|
||||
highScore = leaderboard[board][0].score;
|
||||
}
|
||||
|
||||
void insertLeaderboardEntry(uint8_t board, const char *name, uint16_t scoreValue) {
|
||||
LeaderboardEntry entry = {};
|
||||
strncpy(entry.name, name, NAME_LENGTH);
|
||||
entry.name[NAME_LENGTH] = '\0';
|
||||
entry.score = scoreValue;
|
||||
|
||||
uint8_t insertAt = LEADERBOARD_SIZE;
|
||||
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
|
||||
if (scoreValue > leaderboard[board][index].score) {
|
||||
insertAt = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertAt == LEADERBOARD_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int index = LEADERBOARD_SIZE - 1; index > static_cast<int>(insertAt); --index) {
|
||||
leaderboard[board][index] = leaderboard[board][index - 1];
|
||||
}
|
||||
leaderboard[board][insertAt] = entry;
|
||||
syncHighScoreFromLeaderboard(board);
|
||||
}
|
||||
|
||||
void prepareNameEntry(uint16_t scoreValue) {
|
||||
pendingScore = scoreValue;
|
||||
memset(pendingName, 0, sizeof(pendingName));
|
||||
keyboardRow = 0;
|
||||
keyboardCol = 0;
|
||||
pendingLeaderboardBoard = activeLeaderboardBoardIndex();
|
||||
gameState = NAME_ENTRY;
|
||||
}
|
||||
|
||||
void confirmNameEntry() {
|
||||
if (pendingName[0] == '\0') {
|
||||
strncpy(pendingName, "PLAYER", NAME_LENGTH);
|
||||
pendingName[NAME_LENGTH] = '\0';
|
||||
}
|
||||
insertLeaderboardEntry(pendingLeaderboardBoard, pendingName, pendingScore);
|
||||
saveHighScore();
|
||||
resetGame();
|
||||
gameState = LEADERBOARD;
|
||||
// Prevent accidental immediate input if OK was still held: ignore inputs for 500ms
|
||||
inputIgnoreUntilMs = millis() + 500;
|
||||
lastSettingsInputMs = millis();
|
||||
}
|
||||
|
||||
const char *keyboardKeyLabel(uint8_t row, uint8_t col) {
|
||||
static const char *layout[4][7] = {
|
||||
{"A", "B", "C", "D", "E", "F", "G"},
|
||||
{"H", "I", "J", "K", "L", "M", "N"},
|
||||
{"O", "P", "Q", "R", "S", "T", "U"},
|
||||
{"V", "W", "X", "Y", "Z", "DEL", "OK"}
|
||||
};
|
||||
return layout[row][col];
|
||||
}
|
||||
|
||||
void moveKeyboardCursor(int8_t rowDelta, int8_t colDelta) {
|
||||
keyboardRow = (keyboardRow + 4 + rowDelta) % 4;
|
||||
keyboardCol = (keyboardCol + 7 + colDelta) % 7;
|
||||
}
|
||||
|
||||
void applyKeyboardSelection() {
|
||||
const char *label = keyboardKeyLabel(keyboardRow, keyboardCol);
|
||||
if (strcmp(label, "DEL") == 0) {
|
||||
if (strlen(pendingName) > 0) {
|
||||
pendingName[strlen(pendingName) - 1] = '\0';
|
||||
}
|
||||
} else if (strcmp(label, "OK") == 0) {
|
||||
confirmNameEntry();
|
||||
} else if (strlen(pendingName) < NAME_LENGTH) {
|
||||
const size_t currentLength = strlen(pendingName);
|
||||
pendingName[currentLength] = label[0];
|
||||
pendingName[currentLength + 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void toneMs(uint16_t hz, uint16_t durationMs) {
|
||||
if (toneFreq == 0) return;
|
||||
ledcWriteTone(ledcChannel, hz);
|
||||
delay(durationMs);
|
||||
ledcWriteTone(ledcChannel, 0);
|
||||
}
|
||||
|
||||
uint16_t buzzerCycleUp(uint16_t current) {
|
||||
if (current == 0) return MIN_TONE_FREQ;
|
||||
if (current < MAX_TONE_FREQ) return current + 100;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t buzzerCycleDown(uint16_t current) {
|
||||
if (current == 0) return MAX_TONE_FREQ;
|
||||
if (current > MIN_TONE_FREQ) return current - 100;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void spawnFood() {
|
||||
Cell candidate;
|
||||
do {
|
||||
candidate.x = random(0, GRID_W);
|
||||
candidate.y = random(0, GRID_H);
|
||||
} while (isSnakeCell(candidate));
|
||||
food = candidate;
|
||||
}
|
||||
|
||||
void resetGame() {
|
||||
score = 0;
|
||||
currentDir = RIGHT;
|
||||
nextDir = RIGHT;
|
||||
gameState = START;
|
||||
stateChangeMs = millis();
|
||||
|
||||
snakeLen = 3;
|
||||
const int8_t centerX = GRID_W / 2;
|
||||
const int8_t centerY = GRID_H / 2;
|
||||
snake[0] = {centerX, centerY};
|
||||
snake[1] = {static_cast<int8_t>(centerX - 1), centerY};
|
||||
snake[2] = {static_cast<int8_t>(centerX - 2), centerY};
|
||||
|
||||
spawnFood();
|
||||
lastStepMs = millis();
|
||||
}
|
||||
|
||||
bool anyButtonPressed() {
|
||||
return digitalRead(BTNU) == LOW ||
|
||||
digitalRead(BTNR) == LOW ||
|
||||
digitalRead(BTND) == LOW ||
|
||||
digitalRead(BTNL) == LOW;
|
||||
}
|
||||
|
||||
void updateDirectionFromButtons() {
|
||||
if (digitalRead(BTNU) == LOW && currentDir != DOWN) {
|
||||
nextDir = UP;
|
||||
sprintHoldU++;
|
||||
} else {
|
||||
sprintHoldU = 0;
|
||||
}
|
||||
|
||||
if (digitalRead(BTNR) == LOW && currentDir != LEFT) {
|
||||
nextDir = RIGHT;
|
||||
sprintHoldR++;
|
||||
} else {
|
||||
sprintHoldR = 0;
|
||||
}
|
||||
|
||||
if (digitalRead(BTND) == LOW && currentDir != UP) {
|
||||
nextDir = DOWN;
|
||||
sprintHoldD++;
|
||||
} else {
|
||||
sprintHoldD = 0;
|
||||
}
|
||||
|
||||
if (digitalRead(BTNL) == LOW && currentDir != RIGHT) {
|
||||
nextDir = LEFT;
|
||||
sprintHoldL++;
|
||||
} else {
|
||||
sprintHoldL = 0;
|
||||
}
|
||||
|
||||
isSprinting = sprintAllowed && ((sprintHoldU >= SPRINT_HOLD_FRAMES) ||
|
||||
(sprintHoldR >= SPRINT_HOLD_FRAMES) ||
|
||||
(sprintHoldD >= SPRINT_HOLD_FRAMES) ||
|
||||
(sprintHoldL >= SPRINT_HOLD_FRAMES));
|
||||
}
|
||||
|
||||
uint16_t stepIntervalMs() {
|
||||
if (isSprinting) return SPRINT_STEP_MS;
|
||||
int32_t baseMs = BASE_STEP_MS;
|
||||
uint16_t curSpeedup = SPEEDUP_PER_FOOD_MS;
|
||||
uint16_t minStep = MIN_STEP_MS;
|
||||
uint16_t maxStep = BASE_STEP_MS * 2;
|
||||
switch (difficultyPreset) {
|
||||
case 0:
|
||||
baseMs = BASE_STEP_MS + DIFF_OFFSET_EASY;
|
||||
curSpeedup = DIFF_SPEEDUP_EASY;
|
||||
break;
|
||||
case 1:
|
||||
baseMs = BASE_STEP_MS + DIFF_OFFSET_MED;
|
||||
curSpeedup = DIFF_SPEEDUP_MED;
|
||||
break;
|
||||
case 2:
|
||||
baseMs = BASE_STEP_MS + DIFF_OFFSET_HARD;
|
||||
curSpeedup = DIFF_SPEEDUP_HARD;
|
||||
break;
|
||||
case 3:
|
||||
baseMs = customStepMs;
|
||||
curSpeedup = customSpeedupPerFood;
|
||||
minStep = customMinStepMs;
|
||||
maxStep = customMaxStepMs;
|
||||
break;
|
||||
}
|
||||
|
||||
if (baseMs < (int32_t)minStep) baseMs = minStep;
|
||||
if (baseMs > (int32_t)maxStep) baseMs = maxStep;
|
||||
|
||||
const uint32_t speedDrop = (uint32_t)score * (uint32_t)curSpeedup;
|
||||
if ((uint32_t)baseMs <= (uint32_t)minStep + speedDrop) return minStep;
|
||||
int32_t result = (int32_t)baseMs - (int32_t)speedDrop;
|
||||
if (result < (int32_t)minStep) return minStep;
|
||||
return (uint16_t)result;
|
||||
}
|
||||
|
||||
Cell nextHeadCell() {
|
||||
Cell head = snake[0];
|
||||
switch (currentDir) {
|
||||
case UP: --head.y; break;
|
||||
case RIGHT: ++head.x; break;
|
||||
case DOWN: ++head.y; break;
|
||||
case LEFT: --head.x; break;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
bool outOfBounds(const Cell &cell) {
|
||||
return cell.x < 0 || cell.x >= GRID_W || cell.y < 0 || cell.y >= GRID_H;
|
||||
}
|
||||
|
||||
bool hitsSnakeBody(const Cell &cell) {
|
||||
for (uint16_t index = 0; index < snakeLen; ++index) {
|
||||
if (cellEquals(snake[index], cell)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void runGameStep() {
|
||||
currentDir = nextDir;
|
||||
const Cell newHead = nextHeadCell();
|
||||
|
||||
if (outOfBounds(newHead) || hitsSnakeBody(newHead)) {
|
||||
stateChangeMs = millis();
|
||||
if (qualifiesForLeaderboard(score)) {
|
||||
prepareNameEntry(score);
|
||||
} else {
|
||||
gameState = GAME_OVER;
|
||||
}
|
||||
toneMs(330, 120);
|
||||
toneMs(220, 180);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ateFood = cellEquals(newHead, food);
|
||||
const uint16_t oldLen = snakeLen;
|
||||
uint16_t newLen = oldLen;
|
||||
if (ateFood && snakeLen < MAX_SNAKE) newLen = oldLen + 1;
|
||||
|
||||
for (uint16_t index = newLen - 1; index > 0; --index) {
|
||||
snake[index] = snake[index - 1];
|
||||
}
|
||||
snake[0] = newHead;
|
||||
snakeLen = newLen;
|
||||
|
||||
if (ateFood) {
|
||||
const uint16_t pointsEarned = isSprinting ? 2 : 1;
|
||||
score += pointsEarned;
|
||||
toneMs(toneFreq, 35);
|
||||
spawnFood();
|
||||
}
|
||||
}
|
||||
|
||||
void drawGame() {
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
|
||||
char hudText[32];
|
||||
if (isSprinting) {
|
||||
snprintf(hudText, sizeof(hudText), "S:%u B:%u [SPRINT]", score, highScore);
|
||||
} else {
|
||||
snprintf(hudText, sizeof(hudText), "S:%u B:%u", score, highScore);
|
||||
}
|
||||
u8g2.drawStr(0, 7, hudText);
|
||||
|
||||
for (uint16_t index = 0; index < snakeLen; ++index) {
|
||||
const int16_t x = snake[index].x * CELL;
|
||||
const int16_t y = HUD_H + snake[index].y * CELL;
|
||||
u8g2.drawBox(x, y, CELL, CELL);
|
||||
}
|
||||
|
||||
const int16_t foodX = food.x * CELL;
|
||||
const int16_t foodY = HUD_H + food.y * CELL;
|
||||
u8g2.drawFrame(foodX, foodY, CELL, CELL);
|
||||
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void drawStartScreen() {
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_ncenB08_tr);
|
||||
u8g2.drawStr(32, 20, "SNAKE");
|
||||
|
||||
u8g2.setFont(u8g2_font_4x6_tr);
|
||||
u8g2.drawStr(26, 40, "START: UP");
|
||||
u8g2.drawStr(20, 50, "SETTINGS: RIGHT");
|
||||
if (difficultyPreset == 3) {
|
||||
u8g2.drawStr(4, 60, "NOT AVAILABLE IN CUSTOM");
|
||||
} else {
|
||||
u8g2.drawStr(14, 60, "LEADERBOARD: LEFT");
|
||||
}
|
||||
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void drawGameOverScreen() {
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_ncenB08_tr);
|
||||
u8g2.drawStr(28, 20, "GAME OVER");
|
||||
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
char scoreText[24];
|
||||
snprintf(scoreText, sizeof(scoreText), "Score: %u", score);
|
||||
u8g2.drawStr(28, 35, scoreText);
|
||||
|
||||
char bestText[24];
|
||||
snprintf(bestText, sizeof(bestText), "Best: %u", highScore);
|
||||
u8g2.drawStr(28, 48, bestText);
|
||||
|
||||
u8g2.drawStr(18, 62, "Press any button");
|
||||
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void drawLeaderboardScreen() {
|
||||
const uint8_t board = activeLeaderboardBoardIndex();
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_ncenB08_tr);
|
||||
// char title[28];
|
||||
const char *difficultyNames[3] = {"EASY", "MED ", "HARD"};
|
||||
// snprintf(title, sizeof(title), "LEADERBOARD - %s", difficultyNames[board]);
|
||||
u8g2.drawStr(6, 10, "LEADERBOARD");
|
||||
|
||||
char ch[2] = {0, 0}; // Reusable 1-char string buffer
|
||||
|
||||
if (board == 1) {
|
||||
ch[0] = difficultyNames[board][0]; u8g2.drawStr(93, 26, ch);
|
||||
ch[0] = difficultyNames[board][1]; u8g2.drawStr(98, 36, ch);
|
||||
ch[0] = difficultyNames[board][2]; u8g2.drawStr(103, 46, ch);
|
||||
|
||||
} else {
|
||||
ch[0] = difficultyNames[board][0]; u8g2.drawStr(90, 22, ch);
|
||||
ch[0] = difficultyNames[board][1]; u8g2.drawStr(95, 32, ch);
|
||||
ch[0] = difficultyNames[board][2]; u8g2.drawStr(100, 42, ch);
|
||||
ch[0] = difficultyNames[board][3]; u8g2.drawStr(105, 52, ch);
|
||||
};
|
||||
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
|
||||
char line[24];
|
||||
snprintf(line, sizeof(line), "%u. %-8s %3u", index + 1, leaderboard[board][index].name, leaderboard[board][index].score);
|
||||
u8g2.drawStr(4, 20 + index * 8, line);
|
||||
}
|
||||
|
||||
u8g2.setFont(u8g2_font_4x6_tr);
|
||||
u8g2.drawStr(85, 64, "Back ----->");
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void drawNameEntryScreen() {
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
u8g2.drawStr(2, 8, "NEW RECORD - TYPE NAME");
|
||||
|
||||
char scoreLine[24];
|
||||
snprintf(scoreLine, sizeof(scoreLine), "Score: %u", pendingScore);
|
||||
u8g2.drawStr(2, 16, scoreLine);
|
||||
|
||||
char nameLine[24];
|
||||
snprintf(nameLine, sizeof(nameLine), "Name: %-8s", pendingName);
|
||||
u8g2.drawStr(2, 24, nameLine);
|
||||
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
const uint8_t gridTop = 30;
|
||||
const uint8_t cellW = 18;
|
||||
const uint8_t cellH = 8;
|
||||
|
||||
for (uint8_t row = 0; row < 4; ++row) {
|
||||
for (uint8_t col = 0; col < 7; ++col) {
|
||||
const uint8_t x = col * cellW;
|
||||
const uint8_t y = gridTop + row * cellH;
|
||||
const char *label = keyboardKeyLabel(row, col);
|
||||
const bool selected = (row == keyboardRow && col == keyboardCol);
|
||||
if (selected) {
|
||||
u8g2.drawBox(x + 1, y - 7, cellW - 2, cellH - 1);
|
||||
u8g2.setDrawColor(0);
|
||||
}
|
||||
u8g2.drawStr(x + 5, y, label);
|
||||
if (selected) {
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u8g2.setFont(u8g2_font_4x6_tr);
|
||||
u8g2.drawStr(2, 62, "Move: arrows Select: L+R");
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void drawSettingsScreen() {
|
||||
u8g2.clearBuffer();
|
||||
u8g2.setFont(u8g2_font_ncenB08_tr);
|
||||
u8g2.drawStr(26, 16, "SETTINGS");
|
||||
|
||||
u8g2.setFont(u8g2_font_5x8_tr);
|
||||
// Render a small scroll window (3 rows) with selector fixed in the middle
|
||||
const uint8_t total = settingsTotal();
|
||||
for (uint8_t row = 0; row < SETTINGS_VISIBLE; ++row) {
|
||||
const uint8_t itemIndex = settingsScrollOffset + row;
|
||||
const uint8_t y = 28 + row * 10;
|
||||
bool highlight = (row == (SETTINGS_VISIBLE / 2));
|
||||
if (highlight) {
|
||||
u8g2.drawBox(4, y - 8, 120, 10);
|
||||
u8g2.setDrawColor(0);
|
||||
}
|
||||
|
||||
char line[40] = {0};
|
||||
if (itemIndex == 0 || itemIndex == (total - 1)) {
|
||||
// padding
|
||||
} else {
|
||||
if (total == 6) {
|
||||
// 0 blank,1 buzzer,2 sprint,3 difficulty,4 back,5 blank
|
||||
if (itemIndex == 1) {
|
||||
if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
|
||||
else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
|
||||
} else if (itemIndex == 2) {
|
||||
snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
|
||||
} else if (itemIndex == 3) {
|
||||
const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
|
||||
snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
|
||||
} else if (itemIndex == 4) {
|
||||
snprintf(line, sizeof(line), "Back (save)");
|
||||
}
|
||||
} else {
|
||||
// total == 10 => 0 blank,1 buzzer,2 sprint,3 difficulty,4 customStart,5 customSpeedup,6 customMin,7 customMax,8 back,9 blank
|
||||
if (itemIndex == 1) {
|
||||
if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
|
||||
else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
|
||||
} else if (itemIndex == 2) {
|
||||
snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
|
||||
} else if (itemIndex == 3) {
|
||||
const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
|
||||
snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
|
||||
} else if (itemIndex == 4) {
|
||||
snprintf(line, sizeof(line), " Starting speed: %ums", customStepMs);
|
||||
} else if (itemIndex == 5) {
|
||||
snprintf(line, sizeof(line), " Speedup: %u ms/food", customSpeedupPerFood);
|
||||
} else if (itemIndex == 6) {
|
||||
snprintf(line, sizeof(line), " Min: %ums", customMinStepMs);
|
||||
} else if (itemIndex == 7) {
|
||||
snprintf(line, sizeof(line), " Max: %ums", customMaxStepMs);
|
||||
} else if (itemIndex == 8) {
|
||||
snprintf(line, sizeof(line), "Back (save)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u8g2.drawStr(8, y, line);
|
||||
if (highlight) {
|
||||
u8g2.setDrawColor(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Help line (small)
|
||||
u8g2.setFont(u8g2_font_4x6_tr);
|
||||
u8g2.drawStr(2, 64, "UP/DN: Move L/R: Select/Edit");
|
||||
|
||||
u8g2.sendBuffer();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
pinMode(BTNU, INPUT_PULLUP);
|
||||
pinMode(BTNR, INPUT_PULLUP);
|
||||
pinMode(BTND, INPUT_PULLUP);
|
||||
pinMode(BTNL, INPUT_PULLUP);
|
||||
pinMode(BUZZER, OUTPUT);
|
||||
|
||||
ledcSetup(ledcChannel, 2000, 8);
|
||||
ledcAttachPin(BUZZER, ledcChannel);
|
||||
|
||||
u8g2.begin();
|
||||
randomSeed(micros());
|
||||
loadHighScore();
|
||||
resetGame();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
updateDirectionFromButtons();
|
||||
|
||||
switch (gameState) {
|
||||
case START: {
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
if (difficultyPreset < 3) {
|
||||
lastSettingsInputMs = now;
|
||||
gameState = LEADERBOARD;
|
||||
}
|
||||
} else if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
lastSettingsInputMs = now;
|
||||
gameState = SETTINGS;
|
||||
// prepare scroll so the first selectable item (Buzzer) is under the selector
|
||||
settingsScrollOffset = 0;
|
||||
} else if (now >= inputIgnoreUntilMs && digitalRead(BTNU) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
// Reset the game state before starting to avoid resuming a dead snake
|
||||
resetGame();
|
||||
// Now enter playing state
|
||||
lastSettingsInputMs = now;
|
||||
delay(60);
|
||||
isSprinting = false;
|
||||
sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
|
||||
gameState = PLAYING;
|
||||
stateChangeMs = now;
|
||||
lastStepMs = now;
|
||||
}
|
||||
|
||||
drawStartScreen();
|
||||
break;
|
||||
}
|
||||
|
||||
case SETTINGS: {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Debounce: only process input after a short delay
|
||||
if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
if (digitalRead(BTNU) == LOW) {
|
||||
// UP: scroll up (if possible)
|
||||
if (settingsScrollOffset > 0) settingsScrollOffset--;
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTND) == LOW) {
|
||||
// DOWN: scroll down (if possible)
|
||||
if (settingsScrollOffset + SETTINGS_VISIBLE < settingsTotal()) settingsScrollOffset++;
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTNL) == LOW) {
|
||||
// LEFT: edit focused item
|
||||
const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
|
||||
const uint8_t total = settingsTotal();
|
||||
if (focused == 1) {
|
||||
// Buzzer cycle backward: OFF <- 500 <- ... <- 2000
|
||||
toneFreq = buzzerCycleDown(toneFreq);
|
||||
} else if (focused == 2) {
|
||||
// Sprint toggle
|
||||
sprintAllowed = !sprintAllowed;
|
||||
} else if (focused == 3) {
|
||||
// Cycle difficulty presets backward
|
||||
difficultyPreset = (difficultyPreset + 3) % 4;
|
||||
if (difficultyPreset < 3) {
|
||||
highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
|
||||
} else {
|
||||
highScore = 0;
|
||||
}
|
||||
} else if (focused >= 4 && focused <= 7 && total == 10) {
|
||||
// Custom fields (decrease)
|
||||
if (focused == 4) {
|
||||
if (customStepMs > CUSTOM_MIN_MS) customStepMs -= 10;
|
||||
} else if (focused == 5) {
|
||||
if (customSpeedupPerFood > 1) customSpeedupPerFood -= 1;
|
||||
} else if (focused == 6) {
|
||||
if (customMinStepMs > CUSTOM_MIN_MS) customMinStepMs -= 5;
|
||||
} else if (focused == 7) {
|
||||
if (customMaxStepMs > customMinStepMs + 5) customMaxStepMs -= 5;
|
||||
}
|
||||
} else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
|
||||
// Back
|
||||
saveHighScore();
|
||||
gameState = START;
|
||||
resetGame();
|
||||
}
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTNR) == LOW) {
|
||||
// RIGHT: edit focused item
|
||||
const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
|
||||
const uint8_t total = settingsTotal();
|
||||
if (focused == 1) {
|
||||
// Buzzer cycle forward: OFF -> 500 -> ... -> 2000 -> OFF
|
||||
toneFreq = buzzerCycleUp(toneFreq);
|
||||
} else if (focused == 2) {
|
||||
// Sprint toggle (also allow right to toggle)
|
||||
sprintAllowed = !sprintAllowed;
|
||||
} else if (focused == 3) {
|
||||
// Cycle difficulty presets forward
|
||||
difficultyPreset = (difficultyPreset + 1) % 4;
|
||||
if (difficultyPreset < 3) {
|
||||
highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
|
||||
} else {
|
||||
highScore = 0;
|
||||
}
|
||||
} else if (focused >= 4 && focused <= 7 && total == 10) {
|
||||
// Custom fields (increase)
|
||||
if (focused == 4) {
|
||||
if (customStepMs < CUSTOM_MAX_MS) customStepMs += 10;
|
||||
} else if (focused == 5) {
|
||||
if (customSpeedupPerFood < 255) customSpeedupPerFood += 1;
|
||||
} else if (focused == 6) {
|
||||
if (customMinStepMs < customMaxStepMs - 5) customMinStepMs += 5;
|
||||
} else if (focused == 7) {
|
||||
if (customMaxStepMs < CUSTOM_MAX_MS) customMaxStepMs += 5;
|
||||
}
|
||||
} else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
|
||||
// Back
|
||||
saveHighScore();
|
||||
gameState = START;
|
||||
resetGame();
|
||||
}
|
||||
lastSettingsInputMs = now;
|
||||
}
|
||||
}
|
||||
|
||||
drawSettingsScreen();
|
||||
break;
|
||||
}
|
||||
|
||||
case LEADERBOARD: {
|
||||
const uint32_t now = millis();
|
||||
if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
lastSettingsInputMs = now;
|
||||
gameState = START;
|
||||
settingsScrollOffset = 0;
|
||||
}
|
||||
|
||||
drawLeaderboardScreen();
|
||||
break;
|
||||
}
|
||||
|
||||
case NAME_ENTRY: {
|
||||
const uint32_t now = millis();
|
||||
|
||||
// Require both L+R to be released before allowing another combo press
|
||||
if (digitalRead(BTNL) == HIGH && digitalRead(BTNR) == HIGH) {
|
||||
lrComboReleased = true;
|
||||
}
|
||||
|
||||
// Check L+R combo first (takes priority over movement)
|
||||
if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && digitalRead(BTNR) == LOW && lrComboReleased && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
// latch until both released to avoid repeats
|
||||
applyKeyboardSelection();
|
||||
lrComboReleased = false;
|
||||
lastSettingsInputMs = now;
|
||||
} else if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
|
||||
// Then check individual movement buttons
|
||||
if (digitalRead(BTNU) == LOW) {
|
||||
moveKeyboardCursor(-1, 0);
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTND) == LOW) {
|
||||
moveKeyboardCursor(1, 0);
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTNL) == LOW) {
|
||||
moveKeyboardCursor(0, -1);
|
||||
lastSettingsInputMs = now;
|
||||
} else if (digitalRead(BTNR) == LOW) {
|
||||
moveKeyboardCursor(0, 1);
|
||||
lastSettingsInputMs = now;
|
||||
}
|
||||
}
|
||||
|
||||
drawNameEntryScreen();
|
||||
break;
|
||||
}
|
||||
|
||||
case PLAYING: {
|
||||
const uint32_t now = millis();
|
||||
if (now - lastStepMs >= stepIntervalMs()) {
|
||||
lastStepMs = now;
|
||||
runGameStep();
|
||||
}
|
||||
drawGame();
|
||||
break;
|
||||
}
|
||||
|
||||
case GAME_OVER: {
|
||||
const uint32_t now = millis();
|
||||
// Any button press returns to start/reset the game (debounced)
|
||||
if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS) && anyButtonPressed()) {
|
||||
lastSettingsInputMs = now;
|
||||
delay(60);
|
||||
isSprinting = false;
|
||||
sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
|
||||
resetGame();
|
||||
}
|
||||
drawGameOverScreen();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delay(8);
|
||||
}
|
||||
11
code/test/README
Normal file
11
code/test/README
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
Loading…
Add table
Add a link
Reference in a new issue