From 44c8549dcf41a740880678f8560488f631790fd9 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:14:12 +0000 Subject: [PATCH 1/4] Initial plan From 166ec2e2c4eee35b5b794ae95bbc239435c8a3f9 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:16:13 +0000 Subject: [PATCH 2/4] Document PR #70 changes in CHANGELOG and bump version to 3.1.0 Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com> --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ README.md | 2 +- library.properties | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24133e1..daddabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [3.1.0] — 2026-03-03 + +### Bug Fixes +- **`flappy.h`**: fixed compile error — `rand(28, 40)` (stdlib `rand` takes no arguments) replaced with `random(28, 40)`; bare `rand()%4` replaced with `random(0, 4)`. +- **`snake.h`**: fixed food placement bug — `random(0,7)` changed to `random(0,8)` so food can now appear in column/row 7 (was blocking 12.5% of grid). Food respawn now retries until it lands on empty cell; initial placement avoids snake's spawn cell (0,0). +- **`tetris.h`**: fixed line-clearing score bug — `linesCleared += linesSinceLastDrop` inside loop was accumulating 1+2+3… instead of 1 per line; replaced with `linesCleared++` and removed `linesSinceLastDrop` variable. +- **`breakout.h`**: `saveHighScore()` now called unconditionally on game loss (was incorrectly skipped for scores < 10). +- **`TWSUGamerPlus.ino`**: EEPROM initialization guard changed from `== 0` to `!= 1` — fresh ATmega328P has all bytes at `0xFF`, so high scores incorrectly showed 255 on first boot. Initialization moved before `launcherSetup()`. + +### Improvements +- **`Gamer.cpp`**: removed unused variables `count` and `prevChar` (`-Wunused-variable` warnings); cast `ldrThreshold` to `(int)` in `checkInputs()` to fix `-Wsign-compare` warning; changed loop variables from `int x<=7` to `byte x<8` for consistency. +- **`alien.h`**: changed `long lastMove` to `unsigned long` and `int moveDelay` to `unsigned int` to fix `-Wsign-compare` warnings with `millis()` comparison. +- **`breakout.h`**: applied three changelog items from v3.0.0 that were marked done but never committed — removed 9 spurious `volatile` qualifiers, changed `boolean` to `bool`, renamed `counter` to `breakoutCounter`. +- **`snake.h`**: increased post-loss score display delay from 300 ms to 800 ms (was barely readable after loss tune). +- **`alien.h`**: added `checkSoundToggle()` and `updateLEDFlash()` calls inside the up-to-800 ms blocking `while` loop — sound toggle now works during full game tick. +- **`dino.h`**: replaced `gamer.clear()` (which internally calls `updateDisplay()`) with manual `display[][]` zeroing in render path — eliminated spurious all-black ISR frame between clear and render. + +### Performance / Memory +- **`tetris.h`**: changed `int grid[8][8]` and `int currentPiece[3][3]` to `int8_t` (saves ~73 bytes SRAM); removed 9-line `digitFrom()` helper; changed `long lastDownPressTime` to `unsigned long`. +- **`flappy.h`**: changed `int ticks/birdPos/pipePos/pipeGap` to `unsigned int` or `int8_t` as appropriate. +- **`simon.h`**: changed `byte sequence[30]` to `byte sequence[SIMON_MAX_SEQUENCE]`; changed `int delayMils` to `uint16_t`; removed redundant sequence-zero loop in reset (values always written before read). +- **`breakout.h`** / **`alien.h`**: changed `int` loop variables to `byte` throughout counting loops; countdown loops use `int8_t`. +- **`TWSUGamerPlus.ino`**: changed `GAMER_DEBUG` from `1` to `0` (Serial was unconditionally open); changed `1024` to `EEPROM.length()`. +- **`tetris.h`**: removed duplicate `lastMoveTime = millis()` assignment in gravity branch; `max(300, …)` now uses consistent signedness to avoid implicit conversion. + +--- + ## [3.0.0] — 2026-02-25 ### Bug Fixes diff --git a/README.md b/README.md index f3db5ee..4a36a71 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![CI](https://github.com/28pins/TWSUGamerPlus/actions/workflows/ci.yml/badge.svg) ![Platform: AVR](https://img.shields.io/badge/platform-AVR%20(Uno)-green.svg) ![License: MIT (No AI)](https://img.shields.io/badge/license-MIT%20(No%20AI)-blue.svg) -![Version](https://img.shields.io/badge/version-3.0.0-informational.svg) +![Version](https://img.shields.io/badge/version-3.1.0-informational.svg) Arduino code and a lightweight hardware driver for the Technology Will Save Us DIY Gamer Kit. The repository bundles the `Gamer` library (LED matrix driver, button/IR/buzzer helpers) and a menu-driven sketch that runs fully featured games (not just demos), including added Tetris, richer light/sound effects, and memory optimizations on top of the base example. diff --git a/library.properties b/library.properties index 90aebaa..aace1dc 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=GamerPlus -version=3.0.0 +version=3.1.0 author=28pins maintainer=28pins sentence=Eight-game sketch and hardware driver for the TWSU DIY Gamer Kit. From 39f50b9816675c8bba4e6e256ed8f3708131af80 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:26:38 +0000 Subject: [PATCH 3/4] Reorganize versions: bug fixes to 3.1.1, improvements to 3.1.0; add high score menu docs Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com> --- CHANGELOG.md | 6 +++++- README.md | 7 ++++++- library.properties | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daddabb..81890f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- -## [3.1.0] — 2026-03-03 +## [3.1.1] — 2026-03-03 ### Bug Fixes - **`flappy.h`**: fixed compile error — `rand(28, 40)` (stdlib `rand` takes no arguments) replaced with `random(28, 40)`; bare `rand()%4` replaced with `random(0, 4)`. @@ -14,6 +14,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **`breakout.h`**: `saveHighScore()` now called unconditionally on game loss (was incorrectly skipped for scores < 10). - **`TWSUGamerPlus.ino`**: EEPROM initialization guard changed from `== 0` to `!= 1` — fresh ATmega328P has all bytes at `0xFF`, so high scores incorrectly showed 255 on first boot. Initialization moved before `launcherSetup()`. +--- + +## [3.1.0] — 2026-03-03 + ### Improvements - **`Gamer.cpp`**: removed unused variables `count` and `prevChar` (`-Wunused-variable` warnings); cast `ldrThreshold` to `(int)` in `checkInputs()` to fix `-Wsign-compare` warning; changed loop variables from `int x<=7` to `byte x<8` for consistency. - **`alien.h`**: changed `long lastMove` to `unsigned long` and `int moveDelay` to `unsigned int` to fix `-Wsign-compare` warnings with `millis()` comparison. diff --git a/README.md b/README.md index 4a36a71..af932f9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![CI](https://github.com/28pins/TWSUGamerPlus/actions/workflows/ci.yml/badge.svg) ![Platform: AVR](https://img.shields.io/badge/platform-AVR%20(Uno)-green.svg) ![License: MIT (No AI)](https://img.shields.io/badge/license-MIT%20(No%20AI)-blue.svg) -![Version](https://img.shields.io/badge/version-3.1.0-informational.svg) +![Version](https://img.shields.io/badge/version-3.1.1-informational.svg) Arduino code and a lightweight hardware driver for the Technology Will Save Us DIY Gamer Kit. The repository bundles the `Gamer` library (LED matrix driver, button/IR/buzzer helpers) and a menu-driven sketch that runs fully featured games (not just demos), including added Tetris, richer light/sound effects, and memory optimizations on top of the base example. @@ -63,6 +63,11 @@ On v1.9+ hardware, tap the **capacitive-touch pad** on the PCB to toggle sound o | `START` | Launch the highlighted game | | `START` (in-game) | Exit back to the launcher | +#### High score menu +Pressing `UP` in the launcher displays the current high score for the selected game. High scores are automatically saved to EEPROM when you finish a game. The high score display shows the stored two-digit score with a flashing LED indicator. Press `DOWN` to return to the animated game icon, or press `START` to launch the game from the high score menu. + +High scores persist across power cycles and are protected by CRC-8 checksums to ensure data integrity. The system uses two-slot wear-levelling to extend EEPROM lifespan. + ### Game controls #### Snake diff --git a/library.properties b/library.properties index aace1dc..6215d30 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=GamerPlus -version=3.1.0 +version=3.1.1 author=28pins maintainer=28pins sentence=Eight-game sketch and hardware driver for the TWSU DIY Gamer Kit. From dc2ad5b9c6e407d24ffd1d83ac0e3b0e1c7acfed Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:00:34 +0000 Subject: [PATCH 4/4] Merge main branch and resolve CHANGELOG conflicts Co-authored-by: 28pins <262898015+28pins@users.noreply.github.com> --- CHANGELOG.md | 18 +++++- Gamer.cpp | 90 ++++++++++++++++++++------ Gamer.h | 10 ++- README.md | 17 +++-- TWSUGamerPlus.ino | 8 +++ keywords.txt | 4 ++ src/assets/progmem_assets.h | 7 ++ src/games/alien.h | 3 +- src/games/breakout.h | 123 +++++++++++------------------------- src/games/brightness.h | 59 +++++++++++++++++ src/games/conway.h | 3 +- src/games/dino.h | 25 +++++--- src/games/flappy.h | 4 +- src/games/simon.h | 3 +- src/games/snake.h | 5 +- src/games/tetris.h | 3 +- src/launcher/launcher.h | 3 +- 17 files changed, 249 insertions(+), 136 deletions(-) create mode 100644 src/games/brightness.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 81890f8..656ca8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to TWSUGamerPlus are documented here. +All notable changes to TWSUGamerPlus are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- @@ -16,7 +16,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- -## [3.1.0] — 2026-03-03 +## [3.1.0] — 2026-03-06 + +### Added +- **Brightness control** (`Gamer::setBrightness(uint8_t)` / `getBrightness()`): software-PWM dimming via + the TLC5916 OE pin. Level 1 is dimmest (~12 % duty cycle); level 8 is full brightness (no blanking). + Implemented in `isrRoutine()` by asserting OE HIGH after `brightness × refreshRate / 8` timer ticks. +- **LED brightness compensation** (`Gamer::setLED()` / `toggleLED()`): when the onboard LED (pin 13) + turns on, the effective display brightness is automatically boosted by 3 levels to offset the + voltage-drop dim caused by the LED's current draw on the shared power rail. The boost is reversed + when the LED turns off, restoring the user's chosen level. +- **Brightness settings game** (`src/games/brightness.h`): a launcher-registered settings screen + (icon: pulsing sun) that displays the current brightness as a left-to-right bar of lit columns. + `UP`/`RIGHT` increase, `DOWN`/`LEFT` decrease (1–8), `START` exits. Changes take effect immediately. +- `brightAnim_pgm` (2-frame sun icon) in `src/assets/progmem_assets.h`. +- `setBrightness` and `getBrightness` entries in `keywords.txt`. ### Improvements - **`Gamer.cpp`**: removed unused variables `count` and `prevChar` (`-Wunused-variable` warnings); cast `ldrThreshold` to `(int)` in `checkInputs()` to fix `-Wsign-compare` warning; changed loop variables from `int x<=7` to `byte x<8` for consistency. diff --git a/Gamer.cpp b/Gamer.cpp index 568b351..8c248e2 100644 --- a/Gamer.cpp +++ b/Gamer.cpp @@ -17,6 +17,10 @@ static bool toneIsPlaying = false; static bool playTog = false; static bool toneStopped = false; +// Number of brightness levels added to the display when the onboard LED is on, +// compensating for the voltage-drop dim caused by the LED's current draw. +static constexpr uint8_t LED_BRIGHTNESS_BOOST = 3; + static Gamer *thisGamer = NULL; // Interrupt service routine. @@ -174,6 +178,9 @@ void Gamer::begin() ::thisGamer = this; _refreshRate = 50; + _brightness = 8; + _baseBrightness = 8; + _ledCompensation = true; ldrThreshold = 300; // Setup outputs @@ -301,6 +308,60 @@ void Gamer::setRefreshRate(uint16_t refreshRate) _refreshRate = refreshRate; } +/** + Recalculates the effective ISR brightness from the base level and the current LED state. + Call after any change to _baseBrightness, _ledCompensation, or the LED pin. + */ +void Gamer::updateEffectiveBrightness() +{ + if (digitalRead(PIN_LED) && _ledCompensation) { + uint8_t boosted = _baseBrightness + LED_BRIGHTNESS_BOOST; + _brightness = (boosted > 8) ? 8 : boosted; + } else { + _brightness = _baseBrightness; + } +} + +/** + Sets the display brightness. + @param level brightness from 1 (dimmest) to 8 (full). Values outside this range are clamped. + */ +void Gamer::setBrightness(uint8_t level) +{ + if (level < 1) level = 1; + if (level > 8) level = 8; + _baseBrightness = level; + updateEffectiveBrightness(); +} + +/** + Returns the user-set brightness level (1–8), excluding any active LED compensation boost. + */ +uint8_t Gamer::getBrightness() const +{ + return _baseBrightness; +} + +/** + Enables or disables the automatic brightness boost applied when the onboard LED is on. + When enabled (default), setLED(true) raises the effective brightness by LED_BRIGHTNESS_BOOST + to compensate for the voltage-drop dim caused by the LED's current draw. + @param enabled true to enable compensation, false to disable + */ +void Gamer::setLEDCompensation(bool enabled) +{ + _ledCompensation = enabled; + updateEffectiveBrightness(); +} + +/** + Returns whether LED brightness compensation is currently enabled. + */ +bool Gamer::getLEDCompensation() const +{ + return _ledCompensation; +} + /** Burns the display[][] array onto the display. Call this when you're done changing pixels in your game. */ @@ -383,19 +444,23 @@ void Gamer::printImage(byte* img, int x, int y) /** Sets the value of the red LED. + When LED compensation is enabled (default), turning the LED on boosts the effective + display brightness by LED_BRIGHTNESS_BOOST to offset the voltage-drop dim caused by + the LED's current draw. Compensation can be toggled with setLEDCompensation(). @param value the LED's boolean value */ void Gamer::setLED(bool value) { digitalWrite(PIN_LED, value); + updateEffectiveBrightness(); } /** - Toggles the value of the red LED. + Toggles the value of the red LED (also applies/removes brightness compensation). */ void Gamer::toggleLED() { - digitalWrite(PIN_LED, !digitalRead(PIN_LED)); + setLED(!digitalRead(PIN_LED)); } /** @@ -488,6 +553,10 @@ void Gamer::isrRoutine() if(pulseCount >= _refreshRate) { updateRow(); pulseCount = 0; + } else if (_brightness < 8 && pulseCount == (byte)(((uint16_t)_brightness * _refreshRate) / 8)) { + // Blank after (_brightness/8) of the row period for software-PWM dimming (8 levels total). + // The next updateRow() call will re-enable outputs for the following row. + PORTB |= _BV(PORTB2); } if(pulseCount == _refreshRate/2) { checkInputs(); @@ -542,22 +611,7 @@ void Gamer::appendColumn(byte* screen, byte col) delay(60); } -/** - Shows the score. Maximum 2 digits :( - @param n the score to be displayed - */ -void Gamer::showScore(int n) -{ - byte result[8]; - int dig1=n/10; - int dig2=n%10; - for(byte p=0;p<8;p++) { - result[p]=pgm_read_byte(&allNumbers[dig2][p]); - if( dig1>0 ) - result[p]|=(pgm_read_byte(&allNumbers[dig1][p])<<4); - } - printImage(result); -} +// Removed showScore() — games use the version in the main .ino file instead. /** Prints an 8-byte PROGMEM image onto the display. diff --git a/Gamer.h b/Gamer.h index eafa867..5667713 100644 --- a/Gamer.h +++ b/Gamer.h @@ -39,6 +39,10 @@ class Gamer { // Outputs void setRefreshRate(uint16_t refreshRate); + void setBrightness(uint8_t level); // 1 (dim) – 8 (full); default 8 + uint8_t getBrightness() const; + void setLEDCompensation(bool enabled); // enable/disable the +3 boost when LED is on + bool getLEDCompensation() const; void updateDisplay(); void allOn(); void clear(); @@ -49,8 +53,8 @@ class Gamer { void playTone(int note); void stopTone(); void printString(const char* string); + void showScore(int score); void appendColumn(byte* screen, byte col); - void showScore(int n); void printImagePGM(const byte* pgm_img); // Infrared @@ -77,6 +81,9 @@ class Gamer { // Variables uint16_t _refreshRate; + volatile uint8_t _brightness; // effective level used by ISR (base + LED boost) + uint8_t _baseBrightness; // user-set level (1–8) + bool _ledCompensation; // whether to boost brightness when LED is on bool buttonFlags[6]; unsigned long buttonLastPressed[6]; int lastInputState[6]; @@ -87,6 +94,7 @@ class Gamer { void writeToRegister(byte dataOut); void checkInputs(); void updateRow(); + void updateEffectiveBrightness(); // recalc _brightness from _baseBrightness + LED state int currentInputState[6]; bool tog; diff --git a/README.md b/README.md index af932f9..81d93c3 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,15 @@ Arduino code and a lightweight hardware driver for the Technology Will Save Us D TWSUGamerPlus is an Arduino sketch and accompanying hardware-abstraction library built for the [Technology Will Save Us (TWSU) DIY Gamer Kit](https://www.techwillsaveus.com/shop/diy-kits/diy-gamer-kit-2/). Starting from the base TWSU example, this project adds: - A **fully playable Tetris** implementation with piece rotation, soft-drop, and automatic speed progression. -- **Eight games** in a single sketch: Snake, Breakout, Simon Says, Flappy Bird, Tetris, Space Invaders, Conway’s Game of Life, and Dino Runner. +- **Nine entries** in a single sketch: Snake, Breakout, Simon Says, Flappy Bird, Tetris, Space Invaders, Conway’s Game of Life, Dino Runner, and Brightness Settings. - **Richer audio/visual feedback**: distinct win and loss tunes, non-blocking LED flashes, and per-game sound effects driven by a software tone engine. - A **sound toggle** via the capacitive-touch pad (v1.9+ boards) so players can silence the buzzer without re-flashing. +- **Brightness control** via a dedicated “BRIGHT” settings screen in the launcher — adjustable from 1 (dim) to 8 (full). When the onboard LED turns on (e.g. during high-score view), the display driver automatically boosts brightness by 3 levels to compensate for the LED’s current-draw voltage drop. - **Memory optimisations** that keep the whole program inside the ATmega328P’s 32 KB flash and 2 KB SRAM with room to spare. ## What’s inside -- `Gamer.h` / `Gamer.cpp`: the `Gamer` class that owns the 8x8 display buffer, scans buttons, drives the buzzer and IR LED, and exposes helpers like `printImage`, `printString`, `showScore`, and `playTone`. -- `TWSUGamerPlus.ino`: a single sketch with a launcher and eight games: Snake, Breakout, Simon, Flappy Bird, Tetris, Space Invaders, Conway’s Game of Life, and Dino Runner. +- `Gamer.h` / `Gamer.cpp`: the `Gamer` class that owns the 8x8 display buffer, scans buttons, drives the buzzer and IR LED, and exposes helpers like `printImage`, `printString`, `showScore`, `playTone`, `setBrightness`, and `getBrightness`. +- `TWSUGamerPlus.ino`: a single sketch with a launcher and nine entries: Snake, Breakout, Simon, Flappy Bird, Tetris, Space Invaders, Conway’s Game of Life, Dino Runner, and Brightness Settings. - `library.properties`: Arduino metadata so the folder can live in `~/Arduino/libraries/Gamer`. ## Hardware @@ -126,10 +127,18 @@ This is a zero-player simulation. Watch the cellular automaton evolve from a ran Dodge the scrolling obstacles — ground cacti and flying birds. The game speeds up as your score increases. Colliding with an obstacle ends the run and displays your score. High scores are saved to EEPROM automatically. +#### Brightness Settings +| Button | Action | +|--------|--------| +| `UP` / `RIGHT` | Increase brightness | +| `DOWN` / `LEFT` | Decrease brightness | + +Navigate to the **BRIGHT** entry in the launcher and press `START` to open the settings screen. A bar of lit columns shows the current level (1 column = dimmest, 8 columns = full). The setting takes effect immediately. When the onboard indicator LED turns on (e.g. in the high-score view), the display automatically boosts brightness by 3 levels to compensate for the LED’s current draw; it restores your chosen level when the LED turns off. + ## Gamer library overview - **Setup**: call `gamer.begin()` in `setup()` to configure pins, timers, and defaults. -- **Display**: write pixels into `gamer.display[8][8]` and call `gamer.updateDisplay()` to push them. Helpers: `printImage(byte* img)`, `printImage(img, x, y)`, `printImagePGM(const byte*)`, `allOn()`, `clear()`, `appendColumn()`, `printString(const char*)`, `showScore(int)`, `setRefreshRate(uint16_t)`. +- **Display**: write pixels into `gamer.display[8][8]` and call `gamer.updateDisplay()` to push them. Helpers: `printImage(byte* img)`, `printImage(img, x, y)`, `printImagePGM(const byte*)`, `allOn()`, `clear()`, `appendColumn()`, `printString(const char*)`, `showScore(int)`, `setRefreshRate(uint16_t)`, `setBrightness(uint8_t)` (1–dim to 8–full), `getBrightness()`. - **Inputs**: edge-triggered `isPressed(btn)` for single presses, `isHeld(btn)` for current state, `ldrValue()`/`setldrThreshold()` for light sensing on older boards, `capTouch()` for capacitive input on v1.9 hardware. - **Buzzer**: `playTone(int note)` starts a tone, `stopTone()` stops it. The LED on pin 13 can be toggled with `setLED()`/`toggleLED()`. - **Infrared**: `irBegin()` / `irEnd()` manage the 38 kHz carrier and share timer interrupts with the display refresh logic. diff --git a/TWSUGamerPlus.ino b/TWSUGamerPlus.ino index 971f6e9..563a96d 100644 --- a/TWSUGamerPlus.ino +++ b/TWSUGamerPlus.ino @@ -46,6 +46,13 @@ bool startFromHighScore = false; byte startingHighScore = 0; // ── Helper functions ────────────────────────────────────────────────────────── +// Common game loop preamble: check sound toggle, update LED flash, optionally stop tone +inline void updateGameInput(bool stopTone = true) { + checkSoundToggle(); + updateLEDFlash(); + if (soundEnabled && stopTone) gamer.stopTone(); +} + void startLEDFlash() { gamer.setLED(true); ledFlashStartTime = millis(); @@ -108,6 +115,7 @@ void showScore(byte dig1, byte dig2) { #include "src/games/alien.h" #include "src/games/conway.h" #include "src/games/dino.h" +#include "src/games/brightness.h" // ── Launcher ────────────────────────────────────────────────────────────────── #include "src/launcher/launcher.h" diff --git a/keywords.txt b/keywords.txt index 0b9dfe8..873b57a 100644 --- a/keywords.txt +++ b/keywords.txt @@ -11,6 +11,10 @@ ldrValue KEYWORD2 setldrThreshold KEYWORD2 capTouch KEYWORD2 setRefreshRate KEYWORD2 +setBrightness KEYWORD2 +getBrightness KEYWORD2 +setLEDCompensation KEYWORD2 +getLEDCompensation KEYWORD2 updateDisplay KEYWORD2 allOn KEYWORD2 clear KEYWORD2 diff --git a/src/assets/progmem_assets.h b/src/assets/progmem_assets.h index 47cfd0d..356e783 100644 --- a/src/assets/progmem_assets.h +++ b/src/assets/progmem_assets.h @@ -15,6 +15,7 @@ #define ALIEN_ANIM_FRAMES 2 #define CONWAY_ANIM_FRAMES 2 #define DINO_ANIM_FRAMES 2 +#define BRIGHT_ANIM_FRAMES 2 // ── Launcher animation frames in PROGMEM ──────────────────────────────────── @@ -70,6 +71,12 @@ static const byte dinoAnim_pgm[2][8] PROGMEM = { {B00000000, B00000000, B00000000, B01000000, B01000000, B00000100, B00000100, B11111111} }; +// Frame 0: sun outline (dim); Frame 1: sun filled (bright) +static const byte brightAnim_pgm[2][8] PROGMEM = { + {B00000000, B01000010, B00100100, B00011000, B00011000, B00100100, B01000010, B00000000}, + {B00000000, B01100110, B00111100, B01111110, B01111110, B00111100, B01100110, B00000000} +}; + // ── In-game image assets in PROGMEM ───────────────────────────────────────── // Breakout win/lose frames diff --git a/src/games/alien.h b/src/games/alien.h index e98933c..37a5e35 100644 --- a/src/games/alien.h +++ b/src/games/alien.h @@ -78,8 +78,7 @@ void alienLoop() { moveAlien(); while (millis() - lastMove < moveDelay) { delay(10); - checkSoundToggle(); - updateLEDFlash(); + updateGameInput(); if(gamer.isPressed(UP)){ for(int8_t i = 7; i >= 0; i--) { gamer.display[currentX][i] = 1; diff --git a/src/games/breakout.h b/src/games/breakout.h index 3649857..71c6341 100644 --- a/src/games/breakout.h +++ b/src/games/breakout.h @@ -15,63 +15,44 @@ int origYV=-1; byte scoreBreakout = 0; bool outOfBounds(int xV, int yV) { - return (xV > 8 || xV < 0 || yV > 8 || yV < 0); + return (xV >= 8 || xV < 0 || yV >= 8 || yV < 0); +} + +// Helper: checks if a position is free (LOW) and in bounds +inline bool isFree(int x, int y) { + return !outOfBounds(x, y) && gamer.display[x][y] == LOW; } void physics() { - if(gamer.display[currentXBreakout+velocity[0]][currentYBreakout+velocity[1]]==HIGH || outOfBounds(currentXBreakout+velocity[0],currentYBreakout+velocity[1])) { - //Collided with something!!! + int nextX = currentXBreakout + velocity[0]; + int nextY = currentYBreakout + velocity[1]; + + // Check if we hit something at the next position + if(outOfBounds(nextX, nextY) || gamer.display[nextX][nextY] == HIGH) { + // Collision detected! startLEDFlash(); if(soundEnabled) { - if(currentYBreakout==6) { - gamer.playTone(NOTE_C8); - } - else { - gamer.playTone(NOTE_E8); - } + gamer.playTone((currentYBreakout == 6) ? NOTE_C8 : NOTE_E8); } - if(velocity[0]==1) { - if(velocity[1]==1) { - if(gamer.display[currentXBreakout+velocity[0]][currentYBreakout-1]==LOW && !outOfBounds(currentXBreakout+velocity[0],currentYBreakout-1)) { - velocity[1]=-1; - } - else if(gamer.display[currentXBreakout-1][currentYBreakout-1]==LOW && !outOfBounds(currentXBreakout-1,currentYBreakout-1)) { - velocity[1]=-1; - velocity[0]=-1; - } - } - else if(velocity[1]==-1) { - if(gamer.display[currentXBreakout+velocity[0]][currentYBreakout+1]==LOW && !outOfBounds(currentXBreakout+velocity[0],currentYBreakout+1)) { - velocity[1]=1; - } - else if(gamer.display[currentXBreakout-1][currentYBreakout+1]==LOW && !outOfBounds(currentXBreakout-1,currentYBreakout+1)) { - velocity[1]=1; - velocity[0]=-1; - } - } - } - else if(velocity[0]==-1) { - if(velocity[1]==1) { - if(gamer.display[currentXBreakout+velocity[0]][currentYBreakout-1]==LOW && !outOfBounds(currentXBreakout+velocity[0],currentYBreakout-1)) { - velocity[1]=-1; - } - else if(gamer.display[currentXBreakout+1][currentYBreakout-1]==LOW && !outOfBounds(currentXBreakout-1,currentYBreakout-1)) { - velocity[1]=-1; - velocity[0]=1; - } - } - else if(velocity[1]==-1) { - if(gamer.display[currentXBreakout+velocity[0]][currentYBreakout+1]==LOW && !outOfBounds(currentXBreakout+velocity[0],currentYBreakout+1)) { - velocity[1]=1; - } - else if(gamer.display[currentXBreakout+1][currentYBreakout+1]==LOW && !outOfBounds(currentXBreakout-1,currentYBreakout+1)) { - velocity[1]=1; - velocity[0]=1; - } - } + + // Try to bounce off edges intelligently + // Check if we can bounce just horizontally or just vertically + bool canBounceY = isFree(nextX, currentYBreakout - velocity[1]); + bool canBounceX = isFree(currentXBreakout - velocity[0], nextY); + + if(canBounceY) { + velocity[1] *= -1; // Bounce vertically + } else if(canBounceX) { + velocity[0] *= -1; // Bounce horizontally + } else { + // Corner hit - bounce both directions + velocity[0] *= -1; + velocity[1] *= -1; } - if(!outOfBounds(currentXBreakout+origXV,currentYBreakout+origYV)) { - blocks[currentXBreakout+origXV][currentYBreakout+origYV]=0; + + // Clear the block we hit (if in bounds) + if(!outOfBounds(currentXBreakout + origXV, currentYBreakout + origYV)) { + blocks[currentXBreakout + origXV][currentYBreakout + origYV] = 0; } } } @@ -106,9 +87,7 @@ void startBreakoutReset() { } void breakoutLoop() { - checkSoundToggle(); - updateLEDFlash(); - if(soundEnabled) gamer.stopTone(); + updateGameInput(); if(breakoutCounter>2) { for(byte x=0;x<8;x++) { for(byte y=0;y<8;y++) { @@ -142,42 +121,14 @@ void breakoutLoop() { } } physics(); + // Propagate block destruction to adjacent blocks in a checkerboard pattern for(byte x=0;x<8;x++) { for(byte y=0;y<8;y++) { if(blocks[x][y]==0) { - if(x%2==0) { - if(y%2==0) { - if(x<7) { - blocks[x+1][y]=0; - } else { - blocks[0][y]=0; - } - } - else { - if(x>0) { - blocks[x-1][y]=0; - } else { - blocks[7][y]=0; - } - } - } - else { - if(y%2==0) { - if(x>0) { - blocks[x-1][y]=0; - } else { - blocks[7][y]=0; - } - } - else { - if(x<7) { - blocks[x+1][y]=0; - } else { - blocks[0][y]=0; - } - } - } - } + // Determine adjacent block based on checkerboard pattern + byte adjX = ((x % 2) == (y % 2)) ? ((x < 7) ? x + 1 : 0) : ((x > 0) ? x - 1 : 7); + blocks[adjX][y] = 0; + } } } for(byte x=0;x<8;x++) { diff --git a/src/games/brightness.h b/src/games/brightness.h new file mode 100644 index 0000000..f15ebb8 --- /dev/null +++ b/src/games/brightness.h @@ -0,0 +1,59 @@ +// brightness.h — Brightness settings screen. +// © 2026 28pins — https://github.com/28pins/TWSUGamerPlus +// SPDX-License-Identifier: MIT + +#ifndef BRIGHTNESS_H +#define BRIGHTNESS_H + +// Synced from gamer on entry; written back on every change +static byte _brightSetting = 8; +static bool _ledCompSetting = true; + +// Redraw the settings screen. +// Rows 0–6 (display[col][row] column-major): brightness bar — columns 0..(level-1) lit. +// Row 7: LED compensation indicator — all 8 pixels lit when comp ON, all dark when OFF. +static void drawBrightnessScreen() { + gamer.clear(); + byte bval = gamer.getBrightness(); + // Brightness bar: top 7 rows + for (byte c = 0; c < bval; c++) + for (byte r = 0; r < 7; r++) + gamer.display[c][r] = 1; + // Comp indicator: bottom row + if (gamer.getLEDCompensation()) + for (byte c = 0; c < 8; c++) + gamer.display[c][7] = 1; + gamer.updateDisplay(); +} + +void resetBrightness() { + _brightSetting = gamer.getBrightness(); + _ledCompSetting = gamer.getLEDCompensation(); + drawBrightnessScreen(); +} + +void brightnessLoop() { + bool changed = false; + if (gamer.isPressed(UP) || gamer.isPressed(RIGHT)) { + if (_brightSetting < 8) { + _brightSetting++; + gamer.setBrightness(_brightSetting); + changed = true; + } + } else if (gamer.isPressed(DOWN)) { + if (_brightSetting > 1) { + _brightSetting--; + gamer.setBrightness(_brightSetting); + changed = true; + } + } else if (gamer.isPressed(LEFT)) { + // Toggle LED compensation on/off + _ledCompSetting = !_ledCompSetting; + gamer.setLEDCompensation(_ledCompSetting); + changed = true; + } + if (changed) drawBrightnessScreen(); + delay(150); +} + +#endif // BRIGHTNESS_H diff --git a/src/games/conway.h b/src/games/conway.h index 58baa89..d88baa1 100644 --- a/src/games/conway.h +++ b/src/games/conway.h @@ -72,8 +72,7 @@ bool conwayStepSmall() { void resetConway() { conwayRandomize(); } void conwayLoop() { - checkSoundToggle(); - updateLEDFlash(); + updateGameInput(); bool changed = conwayStep(); if (!changed) { diff --git a/src/games/dino.h b/src/games/dino.h index 323ac4c..616eac4 100644 --- a/src/games/dino.h +++ b/src/games/dino.h @@ -38,9 +38,7 @@ void resetDino() { } void dinoLoop() { - checkSoundToggle(); - updateLEDFlash(); - if (soundEnabled) gamer.stopTone(); + updateGameInput(); // ── Game-over: show score, then auto-restart ────────────────────────── if (dinoOver) { @@ -59,10 +57,17 @@ void dinoLoop() { startLEDFlash(); if (soundEnabled) gamer.playTone(NOTE_A8); } - if (!dinoJumping) { - bool wasDucking = dinoDucking; - dinoDucking = gamer.isHeld(DOWN); - if (dinoDucking && !wasDucking) startLEDFlash(); + // DOWN pressed/held: cancel any active jump and duck + if (gamer.isHeld(DOWN)) { + if (dinoJumping) { + dinoY = 5; + dinoVel = 0; + dinoJumping = false; + } + if (!dinoDucking) startLEDFlash(); + dinoDucking = true; + } else if (!dinoJumping) { + dinoDucking = false; } // ── Tick-gated update ───────────────────────────────────────────────── @@ -84,15 +89,15 @@ void dinoLoop() { // Advance obstacle dinoObsX--; if (dinoObsX < 0) { - dinoObsX = 8; - dinoObsType = (random(3) == 0) ? 1 : 0; // 1/3 bird, 2/3 cactus + dinoObsX = random(8, 11); // 13-18 ticks off-screen (5-10 column gap) + dinoObsType = (random(2) == 0) ? 1 : 0; // 1/3 bird, 2/3 cactus if (dinoObsType == 1 && random(3) == 0) dinoObsType = 2; dinoScore++; if (dinoScore > 99) dinoScore = 99; startLEDFlash(); if (soundEnabled) gamer.playTone(NOTE_E8); - if (dinoSpeed > 60) dinoSpeed -= 5; + if (dinoSpeed > 60) dinoSpeed -= 4; } // Collision (obstacle passes through dino column 1) diff --git a/src/games/flappy.h b/src/games/flappy.h index 4078ddf..0d4303b 100644 --- a/src/games/flappy.h +++ b/src/games/flappy.h @@ -87,9 +87,7 @@ inline void resetFlappyLauncher() { void flappyLoop() { - checkSoundToggle(); - updateLEDFlash(); - if (soundEnabled) gamer.stopTone(); + updateGameInput(); if( menu ) { ++ticks; diff --git a/src/games/simon.h b/src/games/simon.h index 05249ef..d086452 100644 --- a/src/games/simon.h +++ b/src/games/simon.h @@ -28,8 +28,7 @@ void resetSimon() { } void simonLoop() { - checkSoundToggle(); - updateLEDFlash(); + updateGameInput(); static const byte simonNotes[] PROGMEM = {NOTE_E8, NOTE_C8, NOTE_G8, NOTE_D8}; sequence[simonStep]=random(0, SIMON_NUM_DIRECTIONS); if(simonStep>0) { diff --git a/src/games/snake.h b/src/games/snake.h index db03e28..601fcac 100644 --- a/src/games/snake.h +++ b/src/games/snake.h @@ -90,9 +90,8 @@ void snakeRec() { } void snakeLoop() { - checkSoundToggle(); - updateLEDFlash(); - if (soundEnabled) gamer.stopTone(); + updateGameInput(); + // Clear display manually (faster than gamer.clear() which also calls updateDisplay) for(byte x=0;x<8;x++) { for(byte y=0;y<8;y++) { gamer.display[x][y] = LOW; diff --git a/src/games/tetris.h b/src/games/tetris.h index eb40207..95f7078 100644 --- a/src/games/tetris.h +++ b/src/games/tetris.h @@ -187,8 +187,7 @@ void resetTetris() { } void tetrisLoop() { - checkSoundToggle(); - updateLEDFlash(); + updateGameInput(false); // Don't stop tones - Tetris manages its own melody if (soundEnabled && tetrisChirpPending) { gamer.stopTone(); diff --git a/src/launcher/launcher.h b/src/launcher/launcher.h index 00fdb23..5aa0992 100644 --- a/src/launcher/launcher.h +++ b/src/launcher/launcher.h @@ -8,7 +8,7 @@ #include "../assets/progmem_assets.h" // ── Launcher state ──────────────────────────────────────────────────────────── -#define LAUNCHER_MAX_GAMES 8 +#define LAUNCHER_MAX_GAMES 9 static GameDescriptor _games[LAUNCHER_MAX_GAMES]; static byte _numGames = 0; static byte _gameNumber = 0; @@ -37,6 +37,7 @@ void launcherSetup() { registerGame("ALIEN", resetAlienGame, alienLoop, &alienAnim_pgm[0][0], ALIEN_ANIM_FRAMES); registerGame("CONWAY", resetConway, conwayLoop, &conwayAnim_pgm[0][0], CONWAY_ANIM_FRAMES); registerGame("DINO", resetDino, dinoLoop, &dinoAnim_pgm[0][0], DINO_ANIM_FRAMES); + registerGame("BRIGHT", resetBrightness, brightnessLoop, &brightAnim_pgm[0][0], BRIGHT_ANIM_FRAMES); // Initialize Conway simulation for launcher animation conwayRandomize();