/*
* Tetris na matrycę LED 8x8 z Raspberry Pi Pico
* Autor: SunRiver / Lothar Team
* www: https://forum.lothar-team.pl
* Data: 2025-12-28
* Opis: Gra Tetris z animacją startową, końcową i efektami LED.
* Obsługa przycisków: Lewo, Prawo, Dół, Obrót.
*
* Wersja: 1.0
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "ws2812.pio.h"
// ---------- KONFIGURACJA ----------
#define WS2812_PIN 2
#define MATRIX_WIDTH 8
#define MATRIX_HEIGHT 8
#define LED_COUNT (MATRIX_WIDTH * MATRIX_HEIGHT)
#define IS_RGBW false
#define BTN_LEFT 12
#define BTN_RIGHT 13
#define BTN_DOWN 14
#define BTN_ROTATE 15
#define DEBOUNCE_MS 120
#define REPEAT_DELAY_MS 300 // opóźnienie przed autorepeat
#define REPEAT_RATE_MS 90 // szybkość powtarzania
// ---------- BUFOR ----------
uint32_t led_buffer[LED_COUNT];
uint8_t board[MATRIX_HEIGHT][MATRIX_WIDTH];
// ---------- TETROMINO ----------
// klocki Tetrisa (7 typów)
const uint8_t tetrominoes[7][4][4] = {
{ { 0, 0, 0, 0 }, { 1, 1, 1, 1 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// I
{ { 0, 1, 1, 0 }, { 0, 1, 1, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// O
{ { 0, 1, 0, 0 }, { 1, 1, 1, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// T
{ { 0, 1, 1, 0 }, { 1, 1, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// S
{ { 1, 1, 0, 0 }, { 0, 1, 1, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// Z
{ { 1, 0, 0, 0 }, { 1, 1, 1, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } },
// J
{ { 0, 0, 1, 0 }, { 1, 1, 1, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } } // L
};
// Kolory klocków
uint32_t tetris_colors[8] = {
0,
0x00FFFF,
0xFFFF00,
0xA000F0,
0x00FF00,
0xFF0000,
0x0000FF,
0xFFA000
};
// -- Przyciski
typedef struct {
bool held;
absolute_time_t press_time;
absolute_time_t last_repeat;
} button_state_t;
// Stany przycisków L/R
button_state_t btn_left_state = { 0 };
button_state_t btn_right_state = { 0 };
// Sprawdza czy którykolwiek przycisk jest wciśnięty (stan, nie event)
bool any_button_held(void) {
return !gpio_get(BTN_LEFT) ||
!gpio_get(BTN_RIGHT) ||
!gpio_get(BTN_DOWN) ||
!gpio_get(BTN_ROTATE);
}
// Sprawdza czy współrzędne (x,y) są na obwodzie matrycy
static inline bool is_border(int x, int y) {
return x == 0 || x == MATRIX_WIDTH - 1 ||
y == 0 || y == MATRIX_HEIGHT - 1;
}
// ---------- MAPOWANIE ----------
static inline uint32_t xy_to_index(uint8_t x, uint8_t y) {
return (y % 2 == 0) ? y * MATRIX_WIDTH + x
: y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x);
}
// ---------- WYŚWIETLANIE ----------
void show(PIO pio, uint sm) {
for (int i = 0; i < LED_COUNT; i++)
pio_sm_put_blocking(pio, sm, led_buffer[i] << 8u);
}
// Czyści bufor matrycy
void clear_matrix() {
memset(led_buffer, 0, sizeof(led_buffer));
}
// Rysuje planszę na matrycy
void draw_board() {
clear_matrix();
for (int y = 0; y < MATRIX_HEIGHT; y++)
for (int x = 0; x < MATRIX_WIDTH; x++)
if (board[y][x])
led_buffer[xy_to_index(x, y)] = tetris_colors[board[y][x]];
}
// Inicjalizacja PIO dla WS2812
static inline void ws2812_program_init(PIO pio, uint sm, uint offset, uint pin) {
pio_sm_config c = ws2812_program_get_default_config(offset);
sm_config_set_sideset_pins(&c, pin);
sm_config_set_out_shift(&c, false, true, 24);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
float div = clock_get_hz(clk_sys) / (800000 * 10);
sm_config_set_clkdiv(&c, div);
pio_gpio_init(pio, pin);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
// Autorepeat - dla przycisków L/R
bool button_autorepeat(uint pin, button_state_t *state) {
absolute_time_t now = get_absolute_time();
bool pressed = !gpio_get(pin); // pull-up
if (pressed) {
if (!state->held) {
// pierwsze naciśnięcie
state->held = true;
state->press_time = now;
state->last_repeat = now;
return true;
}
// autorepeat
if (absolute_time_diff_us(state->press_time, now) >
REPEAT_DELAY_MS * 1000 &&
absolute_time_diff_us(state->last_repeat, now) >
REPEAT_RATE_MS * 1000) {
state->last_repeat = now;
return true;
}
}
else {
// puszczony
state->held = false;
}
return false;
}
// ---------- LOGIKA ----------
void rotate(uint8_t d[4][4], uint8_t s[4][4]) {
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++)
d[y][x] = s[3 - x][y];
}
// Sprawdza kolizję klocka z planszą lub krawędziami
bool collision(uint8_t p[4][4], int px, int py) {
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++) {
if (!p[y][x]) continue;
int bx = px + x, by = py + y;
if (bx < 0 || bx >= MATRIX_WIDTH || by >= MATRIX_HEIGHT) return true;
if (by >= 0 && board[by][bx]) return true;
}
return false;
}
// Łączy klocek z planszą
void merge_piece(uint8_t p[4][4], int px, int py, uint8_t c) {
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++)
if (p[y][x] && py + y >= 0)
board[py + y][px + x] = c;
}
// ---------- LINIE ----------
void blink_line(int y, PIO pio, uint sm) {
for (int i = 0; i < 3; i++) {
for (int x = 0; x < MATRIX_WIDTH; x++)
led_buffer[xy_to_index(x, y)] = 0;
show(pio, sm);
sleep_ms(120);
for (int x = 0; x < MATRIX_WIDTH; x++)
led_buffer[xy_to_index(x, y)] = 0xFFFFFF;
show(pio, sm);
sleep_ms(120);
}
}
// Sprawdza i usuwa pełne linie
void clear_lines(PIO pio, uint sm) {
for (int y = MATRIX_HEIGHT - 1; y >= 0; y--) {
bool full = true;
for (int x = 0; x < MATRIX_WIDTH; x++)
if (!board[y][x]) full = false;
if (full) {
draw_board();
blink_line(y, pio, sm);
for (int yy = y; yy > 0; yy--)
memcpy(board[yy], board[yy - 1], MATRIX_WIDTH);
memset(board[0], 0, MATRIX_WIDTH);
y++;
}
}
}
// ---------- PRZYCISKI ----------
bool button_pressed(uint pin) {
static absolute_time_t last[32];
absolute_time_t now = get_absolute_time();
if (!gpio_get(pin) &&
absolute_time_diff_us(last[pin], now) > DEBOUNCE_MS * 1000) {
last[pin] = now;
return true;
}
return false;
}
// ---------- START 3-2-1 ----------
const uint8_t digits[3][5][5] = {
{ { 1, 1, 1, 1, 0 }, { 0, 0, 0, 1, 0 }, { 0, 1, 1, 1, 0 }, { 0, 0, 0, 1, 0 }, { 1, 1, 1, 1, 0 } },
{ { 1, 1, 1, 1, 0 }, { 0, 0, 0, 1, 0 }, { 1, 1, 1, 1, 0 }, { 1, 0, 0, 0, 0 }, { 1, 1, 1, 1, 0 } },
{ { 0, 0, 1, 0, 0 }, { 0, 1, 1, 0, 0 }, { 0, 0, 1, 0, 0 }, { 0, 0, 1, 0, 0 }, { 0, 1, 1, 1, 0 } }
};
// ---------- TEXT END ! ----------
const uint8_t font_end[4][7][5] = {
// E
{
{ 1, 1, 1, 1, 1 },
{ 1, 0, 0, 0, 0 },
{ 1, 1, 1, 1, 0 },
{ 1, 0, 0, 0, 0 },
{ 1, 1, 1, 1, 1 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
},
// N
{
{ 1, 0, 0, 0, 1 },
{ 1, 1, 0, 0, 1 },
{ 1, 0, 1, 0, 1 },
{ 1, 0, 0, 1, 1 },
{ 1, 0, 0, 0, 1 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
},
// D
{
{ 1, 1, 1, 1, 0 },
{ 1, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
},
// !
{
{ 0, 1, 0, 0, 0 },
{ 0, 1, 0, 0, 0 },
{ 0, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0 },
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0 }
}
};
// ---------- ANIMACJE ----------
void draw_letter_flash(PIO pio, uint sm, uint8_t letter[7][5], int flashes, int on_ms, int off_ms) {
for (int f = 0; f < flashes; f++) {
// włącz literę
clear_matrix();
for (int y = 0; y < MATRIX_HEIGHT; y++)
for (int x = 0; x < MATRIX_WIDTH; x++)
if (is_border(x, y))
led_buffer[xy_to_index(x, y)] = tetris_colors[(x + y) % 7 + 1];
for (int y = 0; y < 7; y++) {
for (int x = 0; x < 5; x++) {
if (letter[y][x]) {
int px = x * 4 / 5 + 2;
int py = y * 5 / 7 + 1;
led_buffer[xy_to_index(px, py)] = 0xFFFFFF;
}
}
}
show(pio, sm);
sleep_ms(on_ms);
// wyłącz literę (tylko środek)
clear_matrix();
for (int y = 0; y < MATRIX_HEIGHT; y++)
for (int x = 0; x < MATRIX_WIDTH; x++)
if (is_border(x, y))
led_buffer[xy_to_index(x, y)] = tetris_colors[(x + y) % 7 + 1];
show(pio, sm);
sleep_ms(off_ms);
}
}
// Rysuje napis "END !" w centrum matrycy
void draw_word_center(PIO pio, uint sm, int word_index) {
clear_matrix();
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
if (is_border(x, y)) {
int c = (x + y) % 7 + 1;
led_buffer[xy_to_index(x, y)] =
tetris_colors[c];
}
}
}
// GAME / OVER (2x2 litery)
int base_x = 1;
int base_y = 1;
for (int i = 0; i < 4; i++) {
for (int y = 0; y < 5; y++) {
for (int x = 0; x < 5; x++) {
if (font_end[word_index * 4 + i][y][x]) {
led_buffer[
xy_to_index(base_x + (i % 2) * 3 + x / 2,
base_y + (i / 2) * 3 + y / 2)
] = 0xFFFFFF;
}
}
}
}
show(pio, sm);
}
// Animacja startowa 3-2-1
void start_animation(PIO pio, uint sm) {
for (int d = 0; d < 3; d++) {
clear_matrix();
for (int y = 0; y < 5; y++)
for (int x = 0; x < 5; x++)
if (digits[d][y][x])
led_buffer[xy_to_index(x + 1, y + 1)] = 0xFFFFFF;
show(pio, sm);
sleep_ms(600);
clear_matrix();
show(pio, sm);
sleep_ms(150);
}
}
// Wygaszanie środka matrycy, pozostawiając obwód
void fade_center_keep_border(PIO pio, uint sm) {
for (int step = 255; step >= 0; step -= 15) {
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
if (is_border(x, y)) {
// obwód – losowy kolor
int c = (x + y + step) % 7 + 1;
led_buffer[xy_to_index(x, y)] =
tetris_colors[c];
}
else {
// środek – wygaszanie
uint8_t v = step;
led_buffer[xy_to_index(x, y)] =
(v << 16) | (v << 8) | v;
}
}
}
show(pio, sm);
sleep_ms(60);
}
}
// Rysuje literę w centrum matrycy z obwodem
void draw_letter_center(PIO pio, uint sm, uint8_t letter[7][5]) {
clear_matrix();
// rysowanie border
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
if (is_border(x, y)) {
int c = (x + y) % 7 + 1;
led_buffer[xy_to_index(x, y)] = tetris_colors[c];
}
}
}
// rysowanie litery w centrum (skalowanie 5x7 → 4x5)
for (int y = 0; y < 7; y++) {
for (int x = 0; x < 5; x++) {
if (letter[y][x]) {
int px = x * 4 / 5 + 2; // dopasowanie do środka 8x8
int py = y * 5 / 7 + 1;
led_buffer[xy_to_index(px, py)] = 0xFFFFFF;
}
}
}
show(pio, sm);
}
// Pętla animacji napisu "END ! z Flashem"
void game_over_text_loop_flash(PIO pio, uint sm) {
for (int i = 0; i < 4; i++) {
// E, N, D, !
draw_letter_flash(pio, sm, font_end[i], 2, 300, 150);
// 2 razy flash, 300ms włączona, 150ms wyłączona
}
}
// Pętla animacji napisu "END !" bez flasha
void game_over_text_loop(PIO pio, uint sm) {
for (int i = 0; i < 4; i++) {
// E, N, D, !
draw_letter_center(pio, sm, font_end[i]);
sleep_ms(600);
clear_matrix();
show(pio, sm);
sleep_ms(150);
}
}
// ---------- GAME OVER – WĄŻ ----------
void game_over_snake(PIO pio, uint sm) {
clear_matrix();
int left = 0;
int right = MATRIX_WIDTH - 1;
int top = 0;
int bottom = MATRIX_HEIGHT - 1;
int color_idx = 1;
while (left <= right && top <= bottom) {
// górny wiersz →
for (int x = left; x <= right; x++) {
led_buffer[xy_to_index(x, top)] = tetris_colors[color_idx];
show(pio, sm);
sleep_ms(35);
color_idx = (color_idx % 7) + 1;
}
top++;
// prawa kolumna ↓
for (int y = top; y <= bottom; y++) {
led_buffer[xy_to_index(right, y)] = tetris_colors[color_idx];
show(pio, sm);
sleep_ms(35);
color_idx = (color_idx % 7) + 1;
}
right--;
// dolny wiersz ←
if (top <= bottom) {
for (int x = right; x >= left; x--) {
led_buffer[xy_to_index(x, bottom)] = tetris_colors[color_idx];
show(pio, sm);
sleep_ms(35);
color_idx = (color_idx % 7) + 1;
}
bottom--;
}
// lewa kolumna ↑
if (left <= right) {
for (int y = bottom; y >= top; y--) {
led_buffer[xy_to_index(left, y)] = tetris_colors[color_idx];
show(pio, sm);
sleep_ms(35);
color_idx = (color_idx % 7) + 1;
}
left++;
}
}
// Miganie środka wszystkimi kolorami
for (int i = 0; i < 6; i++) {
for (int y = 0; y < MATRIX_HEIGHT; y++)
for (int x = 0; x < MATRIX_WIDTH; x++)
led_buffer[xy_to_index(x, y)] =
tetris_colors[(i + x + y) % 7 + 1];
show(pio, sm);
sleep_ms(180);
}
fade_center_keep_border(pio, sm);
game_over_text_loop_flash(pio, sm);
clear_matrix();
show(pio, sm);
}
// rysowanie pojedyńczego klocka
void draw_piece(uint8_t p[4][4], int px, int py, uint8_t color) {
clear_matrix();
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++)
if (p[y][x] && py + y >= 0)
led_buffer[xy_to_index(px + x, py + y)] =
tetris_colors[color];
}
// animowanie klocka
void rotating_piece_animation(PIO pio, uint sm, int type) {
uint8_t piece[4][4];
uint8_t rotated[4][4];
memcpy(piece, tetrominoes[type], sizeof(piece));
int px = 2; // środek 8x8
int py = 2;
uint8_t color = type + 1;
for (int r = 0; r < 4; r++) {
draw_piece(piece, px, py, color);
show(pio, sm);
for (int i = 0; i < 6; i++) {
sleep_ms(60);
}
rotate(rotated, piece);
memcpy(piece, rotated, sizeof(piece));
}
}
//Pętla animacji startowej (attract mode)
void start_attract_mode(PIO pio, uint sm) {
while (true) {
int type = rand() % 7;
rotating_piece_animation(pio, sm, type);
// SPRAWDZENIE STANU
if (any_button_held()) {
// czekamy aż użytkownik puści przycisk
while (any_button_held())
sleep_ms(10);
sleep_ms(150); // stabilizacja
return;
}
clear_matrix();
show(pio, sm);
sleep_ms(120);
}
}
// Sprawdza czy klocek zablokował się powyżej planszy
bool piece_locked_above_board(int py) {
return py < 0;
}
// Sprawdza czy plansza jest pełna
bool board_full(void) {
for (int y = 0; y < MATRIX_HEIGHT; y++)
for (int x = 0; x < MATRIX_WIDTH; x++)
if (!board[y][x])
return false;
return true;
}
// Czeka na wciśnięcie dowolnego przycisku
void wait_for_any_button(void) {
while (true) {
if (button_pressed(BTN_LEFT) ||
button_pressed(BTN_RIGHT) ||
button_pressed(BTN_DOWN) ||
button_pressed(BTN_ROTATE)) {
sleep_ms(200); // zabezpieczenie przed odbiciem
return;
}
sleep_ms(10);
}
}
// ---------- MAIN ----------
int main() {
stdio_init_all();
sleep_ms(100);
PIO pio = pio0;
uint sm = 0;
uint offset = pio_add_program(pio, &ws2812_program);
ws2812_program_init(pio, sm, offset, WS2812_PIN);
gpio_init(BTN_LEFT); gpio_init(BTN_RIGHT);
gpio_init(BTN_DOWN); gpio_init(BTN_ROTATE);
gpio_set_dir(BTN_LEFT, GPIO_IN);
gpio_set_dir(BTN_RIGHT, GPIO_IN);
gpio_set_dir(BTN_DOWN, GPIO_IN);
gpio_set_dir(BTN_ROTATE, GPIO_IN);
gpio_pull_up(BTN_LEFT);
gpio_pull_up(BTN_RIGHT);
gpio_pull_up(BTN_DOWN);
gpio_pull_up(BTN_ROTATE);
start_attract_mode(pio, sm); // animacja klocków
start_animation(pio, sm); // 3-2-1 po przycisku
memset(board, 0, sizeof(board));
while (true) {
int type = rand() % 7;
uint8_t piece[4][4];
memcpy(piece, tetrominoes[type], 16);
int px = rand() % (MATRIX_WIDTH - 3);
int py = -4;
uint8_t color = type + 1;
// Game Over – brak miejsca na nowy klocek
if (collision(piece, px, py + 1) || board_full()) {
game_over_snake(pio, sm);
wait_for_any_button();
memset(board, 0, sizeof(board));
start_animation(pio, sm);
continue;
}
while (true) {
// --- Sterowanie ---
if (button_autorepeat(BTN_LEFT, &btn_left_state) &&
!collision(piece, px - 1, py))
px--;
if (button_autorepeat(BTN_RIGHT, &btn_right_state) &&
!collision(piece, px + 1, py))
px++;
if (button_pressed(BTN_DOWN) && !collision(piece, px, py + 1))
py++;
if (button_pressed(BTN_ROTATE)) {
uint8_t r[4][4];
rotate(r, piece);
if (!collision(r, px, py))
memcpy(piece, r, 16);
}
// --- Grawitacja ---
if (!collision(piece, px, py + 1)) {
py++;
}
else {
// Klocek się zablokował
// GAME OVER – klocek nie zmieścił się w całości
if (py < 0) {
game_over_snake(pio, sm);
wait_for_any_button();
memset(board, 0, sizeof(board));
start_attract_mode(pio, sm);
wait_for_any_button();
start_animation(pio, sm);
break;
}
merge_piece(piece, px, py, color);
clear_lines(pio, sm);
// GAME OVER – plansza pełna
if (board_full()) {
game_over_snake(pio, sm);
wait_for_any_button();
memset(board, 0, sizeof(board));
start_attract_mode(pio, sm);
wait_for_any_button();
start_animation(pio, sm);
}
break;
}
// --- Rysowanie ---
draw_board();
for (int y = 0; y < 4; y++)
for (int x = 0; x < 4; x++)
if (piece[y][x] && py + y >= 0)
led_buffer[xy_to_index(px + x, py + y)] =
tetris_colors[color];
show(pio, sm);
sleep_ms(200);
}
}
}