Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,55 @@
# 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/).

---

## [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)`.
- **`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()`.

---

## [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.
- **`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
Expand Down
90 changes: 72 additions & 18 deletions Gamer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -174,6 +178,9 @@ void Gamer::begin()
::thisGamer = this;

_refreshRate = 50;
_brightness = 8;
_baseBrightness = 8;
_ledCompensation = true;
ldrThreshold = 300;

// Setup outputs
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion Gamer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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];
Expand All @@ -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;

Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.

Expand All @@ -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
Expand Down Expand Up @@ -63,6 +64,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
Expand Down Expand Up @@ -121,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.
Expand Down
8 changes: 8 additions & 0 deletions TWSUGamerPlus.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=GamerPlus
version=3.0.0
version=3.1.1
author=28pins
maintainer=28pins <https://github.com/28pins>
sentence=Eight-game sketch and hardware driver for the TWSU DIY Gamer Kit.
Expand Down
7 changes: 7 additions & 0 deletions src/assets/progmem_assets.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────

Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/games/alien.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading