/** * \file tetris.c * \brief Simple gaming board - Tetris * * \subsection Copyright * Copyright (c) 2013-2022 Michael Buesch * * \subsection License * Licensed under the terms of the GNU General Public License version 2. */ #include "tetris.h" #include "ks0108.h" #include "shr3.h" #include "util.h" #include "music.h" #include "font.h" #include #include /** Tetris Spielzustand. */ enum tetris_state { /** Im Intro. */ TETRIS_INTRO = 0, /** Im Spiel. */ TETRIS_INGAME, /** Game over */ TETRIS_GAMEOVER, }; /** Tetris Konstanten */ enum tetris_constants { /** Breite des Spielbretts, in Steinen. */ BOARD_WIDTH = 8, /** Hoehe des Spielbretts, in Steinen. */ BOARD_HEIGHT = 16, /** Maximale Breite und Hoehe eines Blocks, in Steinen. */ MAX_BLOCK_SIZE = 4, /** Breite und Hoehe eines Steins, in Pixel. */ TOKEN_SIZE_PIX = 8, /** Naechstes Level nach X geloeschten Zeilen. */ NR_ZAPPED_NEXT_LEVEL = 4, /** Multiplikator fuer Punktzahl. */ SCORE_MUL = 8, }; /** 100 Hz Zeitgeber-Intervalle. */ enum tetris_timer_intervals { /** Normaler Intervall. */ TIMER_INTER_DEFAULT = 100, /** Intervall fuer schnell fallenden Block. */ TIMER_INTER_FASTDOWN = 5, /** Intervall fuer links/rechts-Bewegungen. */ TIMER_INTER_MOVE = 13, /** Intervall fuer Lautstaerke hoch/runter. */ TIMER_INTER_VOLUPDOWN = 30, }; /** Stein-flags. */ enum token_flags { /** Stein wird angezeigt. */ TOKEN_FILLED = (1 << 0), }; /** Ein Tetris "Stein". */ struct tetris_token { uint8_t flags; }; /** Ein Tetris "Block" (mehrere "Steine"). */ struct tetris_block { /* Die Steine des Blocks */ struct tetris_token tokens[MAX_BLOCK_SIZE][MAX_BLOCK_SIZE]; /* Die X Koordinate des Blocks im Spielfeld. */ uint8_t x; /* Die Y Koordinate des Blocks im Spielfeld. */ uint8_t y; /* Die Breite des Blocks (<= MAX_BLOCK_SIZE). */ uint8_t width; /* Die Hoehe des Blocks (<= MAX_BLOCK_SIZE). */ uint8_t height; }; /** Zeilen-flags. */ enum line_flags { /** Zeile wurde veraendert. */ LINE_MODIFIED = (1 << 0), }; /** Ein Tetris "Spielfeld". * * Der Nullpunkt ist oben links: * * 0#### x (8) > * ##### * ##### * ##### * y (16) # v */ struct tetris_board { /** Zweidimensionales Spielfeld */ struct tetris_token tokens[BOARD_HEIGHT][BOARD_WIDTH]; /** Zeilenflags. */ uint8_t line_flags[BOARD_HEIGHT]; }; /** Tetris Spielstatus */ struct tetris_game { /** Der Spielstatus. */ enum tetris_state state; /** Das Spielfeld. */ struct tetris_board board; /** Ist ein fallender Block vorhanden? */ bool have_falling_block; /** Der fallende Block. */ struct tetris_block falling_block; /** Fallenden Block schnell nach unten bewegen. */ bool fast_down; /** Der Spieltimer zaehler. * Zaehlt mit 100Hz runter. */ uint8_t timer_count; /** Spieltimer Intervall. */ uint8_t timer_inter; /** Anzahl der geloeschten vollen Zeilen. */ uint8_t nr_zapped; /** Schwierigkeitsgrad. */ uint8_t level; /** Gesamtpunktzahl. */ uint16_t score; /** LCD Update Flag. */ bool lcd_update; }; #define TOK(_flags) { .flags = (_flags), } #define FILLED TOK(TOKEN_FILLED) #define EMPTY TOK(0) static const struct tetris_block __flash tetris_blocks[] = { { /* Block: #### */ .tokens = { { FILLED, FILLED, FILLED, FILLED, }, }, .width = 4, .height = 1, }, { /* Block: # * ### */ .tokens = { { FILLED, EMPTY, EMPTY, }, { FILLED, FILLED, FILLED, }, }, .width = 3, .height = 2, }, { /* Block: # * ### */ .tokens = { { EMPTY, EMPTY, FILLED, }, { FILLED, FILLED, FILLED, }, }, .width = 3, .height = 2, }, { /* Block: ## * ## */ .tokens = { { FILLED, FILLED, }, { FILLED, FILLED, }, }, .width = 2, .height = 2, }, { /* Block: ## * ## */ .tokens = { { EMPTY, FILLED, FILLED, }, { FILLED, FILLED, EMPTY, }, }, .width = 3, .height = 2, }, { /* Block: ## * ## */ .tokens = { { FILLED, FILLED, EMPTY, }, { EMPTY, FILLED, FILLED, }, }, .width = 3, .height = 2, }, { /* Block: # * ### */ .tokens = { { EMPTY, FILLED, EMPTY, }, { FILLED, FILLED, FILLED, }, }, .width = 3, .height = 2, }, }; /** Instanz des Spielstatus. */ static struct tetris_game tetris; /** \brief Einen Zufallsblock generieren. * * \param block Der Zielpuffer fuer den zufaelligen Block. */ static void get_random_block(struct tetris_block *block) { uint8_t index; /* Zufaelligen Index generieren. */ index = shr3_get_random_value_byte(ARRAY_SIZE(tetris_blocks) - 1); /* Block am Index in Puffer kopieren. */ *block = tetris_blocks[index]; } /** \brief Block um 90 Grad rotieren. * * \param to_block Der Puffer in den der rotierte Block geschrieben wird. * * \param from_block Der Block der rotiert werden soll. * * \param direction 1 -> Nach rechts rotieren. * -1 -> Nach links rotieren. */ static void block_rotate(struct tetris_block *to_block, const struct tetris_block *from_block, int8_t direction) { uint8_t x, y; int8_t sx, sy; uint8_t w, h; int8_t offs; if (!direction) { /* Keine Rotation. */ *to_block = *from_block; return; } if (direction < 0) { /* Rotation nach links. */ for (y = 0; y < from_block->height; y++) { for (x = 0; x < from_block->width; x++) { to_block->tokens[from_block->width - x - 1][y] = from_block->tokens[y][x]; } } } else { /* Rotation nach rechts. */ for (y = 0; y < from_block->height; y++) { for (x = 0; x < from_block->width; x++) { to_block->tokens[x][from_block->height - y - 1] = from_block->tokens[y][x]; } } } sx = (int8_t)from_block->x; sy = (int8_t)from_block->y; /* Hoehe/Breite tauschen. */ h = from_block->width; w = from_block->height; /* Position korrigieren. */ offs = ((int8_t)h - (int8_t)w) / 2; sx += offs; sy -= offs; /* Randlage korrigieren. */ if (sx < 0) sx = 0; if (sx + (int8_t)w > BOARD_WIDTH) sx = BOARD_WIDTH - w; if (sy < 0) sy = 0; if (sy + (int8_t)h > BOARD_HEIGHT) sy = BOARD_HEIGHT - h; to_block->x = (uint8_t)sx; to_block->y = (uint8_t)sy; to_block->height = h; to_block->width = w; } /** \brief Einen Spielstein im Bitmap zeichnen. */ static void draw_token(struct bitmap *bitmap, uint8_t x_offset, uint8_t y_offset, const struct tetris_token *t) { /* Hintergrundfarbe zeichnen: * Schwarz wenn Stein sichtbar, sonst weiss. */ bitmap_area_fill(bitmap, x_offset, y_offset, TOKEN_SIZE_PIX, TOKEN_SIZE_PIX, !!(t->flags & TOKEN_FILLED)); /* Linken Rand weiss zeichnen. */ bitmap_draw_line(bitmap, x_offset, y_offset, x_offset + TOKEN_SIZE_PIX - 1, y_offset, 0); /* Oberen Rand weiss zeichnen. */ bitmap_draw_line(bitmap, x_offset + TOKEN_SIZE_PIX - 1, y_offset, x_offset + TOKEN_SIZE_PIX - 1, y_offset + TOKEN_SIZE_PIX - 1, 0); } /** \brief Prueft ob die gegebenenen globalen Koordinaten sich * innerhalb eines Blocks befinden. * * \param block Der Block. * * \param line Die globale Y Koordinate. * * \param col Die globale X Koordinate. * * \return Gibt 1 zurueck, wenn die Koordinaten im Block sind. * Sonst 0. */ static bool coord_is_in_block(const struct tetris_block *block, uint8_t line, uint8_t col) { if (col < block->x || col >= block->x + block->width) return 0; if (line < block->y || line >= block->y + block->height) return 0; return 1; } /** \brief Pruefen ob ein Block mit Steinen auf dem Spielbrett kollidiert. * * \param block Der Block. * * \return Gibt 1 zurueck, wenn der Block kollidiert. Sonst 0. */ static bool check_block_collision(const struct tetris_block *block) { const struct tetris_token *t_block, *t_board; uint8_t x, y; if (block->y >= BOARD_HEIGHT || block->y + block->height > BOARD_HEIGHT || block->x >= BOARD_WIDTH || block->x + block->width > BOARD_WIDTH) { /* Kollision mit Rand. */ return 1; } /* Jeden Stein des Blocks pruefen. */ for (y = 0; y < block->height; y++) { for (x = 0; x < block->width; x++) { t_block = &block->tokens[y][x]; t_board = &tetris.board.tokens[block->y + y][block->x + x]; if ((t_block->flags & TOKEN_FILLED) && (t_board->flags & TOKEN_FILLED)) { /* Sowohl der Block-Stein als auch der * Spielfeld-Stein sind gefuellt. * -> Kollision. */ return 1; } } } /* Keine Kollision. */ return 0; } /** \brief Das "veraendert" Flag fuer eine Zeile setzen. * * \param line Die Zeilennummer. */ static void line_set_modified(uint8_t line) { /* "veraendert" Flag fuer Zeile setzen. */ tetris.board.line_flags[line] |= LINE_MODIFIED; /* LCD update flag setzen. */ tetris.lcd_update = 1; } /** \brief Das "veraendert" Flag fuer alle Zeilen setzen. */ static void all_lines_set_modified(void) { uint8_t line; for (line = 0; line < BOARD_HEIGHT; line++) line_set_modified(line); } /** \brief Das "veraendert" Flag fuer alle Zeilen auf denen * der Block liegt setzen. * * \param block Der Block. */ static void block_lines_set_modified(const struct tetris_block *block) { uint8_t y; for (y = 0; y < block->height; y++) line_set_modified(block->y + y); } /** \brief Kopiert den Inhalt einer Zeile in eine andere. * * \param to_line Die Zielzeile, in die kopiert werden soll. * Wenn to_line ausserhalb des gueltigen Bereiches * liegt, wird nicht kopiert. * * \param from_line Die Quellzeile, von der kopiert werden soll. * Wenn from_line ausserhalb des gueltigen Bereiches * liegt, wird to_line geloescht. */ static void line_copy(int8_t to_line, int8_t from_line) { if (to_line == from_line) { /* Quell- und Zielzeile sind gleich. * -> Nicht kopieren. */ return; } if (to_line < 0 || to_line >= BOARD_HEIGHT) { /* Zielzeile ist ausserhalb des Boards. * -> Nicht kopieren. */ return; } if (from_line < 0 || from_line >= BOARD_HEIGHT) { /* Quellzeile ist ausserhalb des Boards. * -> Zielzeile "nullen". */ memset(&tetris.board.tokens[to_line][0], 0, BOARD_WIDTH * sizeof(struct tetris_token)); } else { /* Quell- und Zielzeile sind ok. * -> kopieren. */ memcpy(&tetris.board.tokens[to_line][0], &tetris.board.tokens[from_line][0], BOARD_WIDTH * sizeof(struct tetris_token)); } /* "veraendert" Flag fuer die Zielzeile setzen. */ line_set_modified(to_line); } /** \brief Pruefen ob eine Zeile voll und zusammenhaengend ist. * Die Zeile ist voll und zusammenhaengend, wenn alle Steine * gefuellt sind. * * \param line Die Zeilennummer. */ static bool line_is_coherent(uint8_t line) { uint8_t col, flags; /* Alle TOKEN_FILLED Flags der Zeile ver-UND-en. */ flags = TOKEN_FILLED; for (col = 0; col < BOARD_WIDTH; col++) flags &= tetris.board.tokens[line][col].flags; /* Wenn TOKEN_FILLED immer noch gesetzt ist, dann waren * alle TOKEN_FILLED Flags in der Zeile gesetzt. */ return !!(flags & TOKEN_FILLED); } /** \brief Zusammenhaengende Zeilen im Spielfeld loeschen * und den Rest nachruecken. */ static void zap_coherent_lines(void) { int8_t from_line, to_line; bool line_zapped = 0, speed_increased = 0; /* Unten anfangen. */ from_line = BOARD_HEIGHT - 1; to_line = BOARD_HEIGHT - 1; /* Alle Zeilen durchlaufen. */ for ( ; from_line >= -1; from_line--) { if (from_line >= 0 && line_is_coherent(from_line)) { /* Zeile ist zusammenhaengend. * Zeile nicht kopieren (= loeschen). */ /* "zap"-Merker setzen. */ line_zapped = 1; /* Gesamtpunktzahl erhoehen. */ if (tetris.score <= UINT16_MAX - (tetris.level * SCORE_MUL)) tetris.score += tetris.level * SCORE_MUL; /* Naechstes Level? */ tetris.nr_zapped++; if (tetris.nr_zapped >= NR_ZAPPED_NEXT_LEVEL) { tetris.nr_zapped = 0; /* Geschwindigkeit und Level erhoehen. */ if (tetris.timer_inter > 10) tetris.timer_inter -= 10; if (tetris.level < UINT8_MAX / SCORE_MUL) tetris.level++; /* "geschwindigkeit erhoeht"-Merker setzen. */ speed_increased = 1; } continue; } /* Zeile kopieren. */ line_copy(to_line, from_line); to_line--; } /* Ggf. Ton ausgeben. */ if (speed_increased) buzzer_play(sound_incspeed, 1500, 0); else if (line_zapped) buzzer_play(sound_zapline, 1500, 0); } /** \brief Einen Block mit dem Spielfeld verschmelzen. * * \param block Der Block der verschmolzen werden soll. */ static void merge_block_with_board(const struct tetris_block *block) { uint8_t x, y; /* Alle Zeilen auf denen der Block liegt als "veraendert" markieren. */ block_lines_set_modified(block); /* Jeden gefuellten Stein des Blocks auf das Spielfeld kopieren. */ for (y = 0; y < block->height; y++) { for (x = 0; x < block->width; x++) { if (!(block->tokens[y][x].flags & TOKEN_FILLED)) { /* Stein ist nicht gefuellt. */ continue; } /* Stein ins Spielfeld kopieren. */ tetris.board.tokens[block->y + y][block->x + x] = block->tokens[y][x]; } } } /** \brief Verschmelzt den fallenden Block mit dem Spielfeld. */ static void merge_falling_block(void) { /* Stein verschmelzen. */ merge_block_with_board(&tetris.falling_block); /* Fallender-Block-Merker ruecksetzen. */ tetris.have_falling_block = 0; /* Zusammenhaengende Zeilen finden, zaehlen und loeschen. */ zap_coherent_lines(); } /** \brief Schiebt den fallenden Block um eine Einheit herunter. */ static void falling_block_move_down(void) { /* Position des Blocks inkrementieren. (= nach unten schieben) */ tetris.falling_block.y++; /* Auf Kollision pruefen. */ if (check_block_collision(&tetris.falling_block)) { /* Kollision! Block ist am Ende angekommen. * Block in der vorherigen Position mit dem * Spielfeld verschmelzen. */ tetris.falling_block.y--; merge_falling_block(); tetris.fast_down = false; } else { /* Keine Kollision. * Veraenderte Zeilen als "veraendert" markieren. */ line_set_modified(tetris.falling_block.y - 1); block_lines_set_modified(&tetris.falling_block); } } /** \brief Tetris Spielbrett auf LCD neuzeichnen. */ static void tetris_game_screen_update(void) { DEFINE_BITMAP(line_bitmap, TOKEN_SIZE_PIX, BOARD_WIDTH * TOKEN_SIZE_PIX); struct tetris_token *t; uint8_t line, col; uint8_t x; /* Spielfeld Zeilenweise zeichnen und auf LCD ausgeben */ for (line = 0; line < BOARD_HEIGHT; line++) { if (!(tetris.board.line_flags[line] & LINE_MODIFIED)) { /* Zeile wurde nicht veraendert. * -> nicht neuzeichnen. */ continue; } /* Eine Zeile zeichnen. */ for (col = 0; col < BOARD_WIDTH; col++) { t = NULL; /* Wenn wir im Bereich des fallenden Blocks sind, * Steine aus diesem zeichnen. */ if (tetris.have_falling_block && coord_is_in_block(&tetris.falling_block, line, col)) { t = &tetris.falling_block.tokens [line - tetris.falling_block.y] [col - tetris.falling_block.x]; if (!(t->flags & TOKEN_FILLED)) t = NULL; } /* Wenn nicht im Bereich des fallenden Blocks, * Steine aus dem Spielfeld zeichnen. */ if (!t) t = &tetris.board.tokens[line][col]; /* Den Stein zeichnen. */ draw_token(BITMAP_PTR(line_bitmap), 0, col * TOKEN_SIZE_PIX, t); } /* Zeile auf LCD ausgeben. */ x = (BOARD_HEIGHT - line - 1) * TOKEN_SIZE_PIX; ks0108_draw_bitmap(BITMAP_PTR(line_bitmap), x, 0); /* "veraendert" Flag fuer Zeile ruecksetzen. */ tetris.board.line_flags[line] &= ~LINE_MODIFIED; } } /** \brief Intro auf LCD neuzeichnen. */ static void tetris_intro_screen_update(void) { struct tetris_block block; DEFINE_BITMAP(bm_horiz, 14, 48); DEFINE_BITMAP(bm_vert, 23, 16); char buffer[12]; /* Z-Block zeichnen. */ block = tetris_blocks[6]; block.x = 3; block.y = 0; merge_block_with_board(&block); /* L-Block zeichnen. */ block = tetris_blocks[1]; block.x = 1; block.y = 9; merge_block_with_board(&block); /* O-Block zeichnen. */ block = tetris_blocks[3]; block.x = 5; block.y = 12; merge_block_with_board(&block); /* Bloecke auf LCD ausgeben. */ tetris_game_screen_update(); /* Horizontales "Tetr" auf LCD zeichnen. */ BITMAP_PTR(bm_horiz)->rotation = BITMAP_ROT_90DEG; bitmap_draw_string(BITMAP_PTR(bm_horiz), 0, 0, &font_10x14, to_memx(PSTR("Tetr"))); ks0108_draw_bitmap(BITMAP_PTR(bm_horiz), 79, 8); /* Vertikales "is" auf LCD zeichnen. */ BITMAP_PTR(bm_vert)->rotation = BITMAP_ROT_180DEG; bitmap_draw_string(BITMAP_PTR(bm_vert), 0, 0, &font_10x14, to_memx(PSTR("is"))); ks0108_draw_bitmap(BITMAP_PTR(bm_vert), 55, 40); /* Versionsnummer auf LCD zeichnen. */ bitmap_clear(BITMAP_PTR(bm_horiz)); buffer[0] = 'v'; utoa(VERSION, buffer + 1, 10); bitmap_draw_string(BITMAP_PTR(bm_horiz), 0, 0, &font_10x14, to_memx(buffer)); ks0108_draw_bitmap(BITMAP_PTR(bm_horiz), 0, 0); } /** \brief Gameover Status auf LCD neuzeichnen. */ static void tetris_gameover_screen_update(void) { DEFINE_BITMAP(bm, 18, 64); char buffer[6]; /* Bitmap rotieren. */ BITMAP_PTR(bm)->rotation = BITMAP_ROT_90DEG; /* String "Score" auf LCD ausgeben. */ bitmap_draw_string(BITMAP_PTR(bm), 0, 2, &font_10x14, to_memx(PSTR("Score"))); ks0108_draw_bitmap(BITMAP_PTR(bm), 79, 0); /* Bitmap loeschen. */ bitmap_clear(BITMAP_PTR(bm)); /* Anzahl der geloeschten Zeilen als Score ausgeben. */ utoa(tetris.score, buffer, 10); bitmap_draw_string(BITMAP_PTR(bm), 0, 2, &font_10x14, to_memx(buffer)); ks0108_draw_bitmap(BITMAP_PTR(bm), 61, 0); } /** \brief LCD Update durchfuehren, wenn noetig. */ static void tetris_lcd_update(void) { if (!tetris.lcd_update) { /* Kein LCD Update angefordert. */ return; } /* LCD Update je nach Spielzustand durchfuehren. */ switch (tetris.state) { case TETRIS_INTRO: tetris_intro_screen_update(); break; case TETRIS_INGAME: tetris_game_screen_update(); break; case TETRIS_GAMEOVER: tetris_gameover_screen_update(); break; } /* LCD Update Merker ruecksetzen. */ tetris.lcd_update = 0; } /** \brief Einen neuen "fallenden Block" erzeugen. */ static void create_new_falling_block(void) { /* Fallender-Block-Merker ruecksetzen. */ tetris.have_falling_block = 1; /* Zufaelligen fallenden Block generieren. */ get_random_block(&tetris.falling_block); /* Block in X zentrieren. */ tetris.falling_block.x = BOARD_WIDTH / 2 - tetris.falling_block.width / 2; /* Zeilen als "veraendert" markieren. */ block_lines_set_modified(&tetris.falling_block); } /** \brief TETRIS_INTRO Status einleiten. */ static void tetris_enter_state_intro(void) { /* Spielbrett loeschen. */ memset(&tetris.board, 0, sizeof(tetris.board)); /* Alle Zeilen als "veraendert" markieren. */ all_lines_set_modified(); /* Spielstatus initialisieren. */ tetris.state = TETRIS_INTRO; tetris.have_falling_block = 0; tetris.lcd_update = 1; /* Intro-Musik abspielen. */ buzzer_play(music_korobeiniki, 1500, 1); } /** \brief TETRIS_INGAME Status einleiten. */ static void tetris_enter_state_ingame(void) { /* Spielbrett loeschen. */ memset(&tetris.board, 0, sizeof(tetris.board)); /* Alle Zeilen als "veraendert" markieren. */ all_lines_set_modified(); /* Spielstatus initialisieren. */ tetris.state = TETRIS_INGAME; tetris.timer_inter = TIMER_INTER_DEFAULT; tetris.timer_count = tetris.timer_inter; create_new_falling_block(); tetris.nr_zapped = 0; tetris.score = 0; tetris.level = 1; tetris.lcd_update = 1; /* Intro-Musik abbrechen. */ buzzer_stop(); } /** \brief TETRIS_GAMEOVER Status einleiten. */ static void tetris_enter_state_gameover(void) { /* Spielstatus initialisieren. */ tetris.state = TETRIS_GAMEOVER; tetris.lcd_update = 1; } /** \brief Spieltimer verarbeiten. */ static void tetris_timer_handle(void) { if (tetris.have_falling_block) { /* Fallenden Block nach unten verschieben. */ falling_block_move_down(); } else { /* Einen neuen fallenden Block erzeugen. */ create_new_falling_block(); /* Auf Kollision des neuen Blocks pruefen. */ if (check_block_collision(&tetris.falling_block)) { /* Neuer Block kollidiert sofort nach der Erstellung. * -> Game over. */ merge_block_with_board(&tetris.falling_block); /* Spielfeld neuzeichnen. */ tetris_game_screen_update(); /* Game-over Ton abspielen. */ buzzer_play(sound_gameover, 1500, 0); /* Spielstatus auf Game-over setzen. */ tetris_enter_state_gameover(); } } } /** \brief Tasterereignis fuer das Spiel verarbeiten. */ static void tetris_game_handle_button(const struct button_event *event) { struct tetris_block rotated_block; switch (event->button) { case BTN_UP: break; case BTN_LEFT: if (event->action != BTNACT_PRESSED) break; if (!tetris.have_falling_block) break; /* "links"-Taster gedrueckt. * Fallenden Block nach links verschieben. */ tetris.falling_block.x--; if (check_block_collision(&tetris.falling_block)) tetris.falling_block.x++; else block_lines_set_modified(&tetris.falling_block); break; case BTN_RIGHT: if (event->action != BTNACT_PRESSED) break; if (!tetris.have_falling_block) break; /* "rechts"-Taster gedrueckt. * Fallenden Block nach rechts verschieben. */ tetris.falling_block.x++; if (check_block_collision(&tetris.falling_block)) tetris.falling_block.x--; else block_lines_set_modified(&tetris.falling_block); break; case BTN_DOWN: if (event->action == BTNACT_PRESSED) { if (!tetris.have_falling_block) break; /* "unten"-Taster gedrueckt. * Fallenden Block schnell nach unten verschieben. */ tetris.timer_count = min(TIMER_INTER_FASTDOWN, tetris.timer_inter); tetris.fast_down = true; } else { tetris.fast_down = false; } break; case BTN_A: if (event->action != BTNACT_PRESSED) break; if (!tetris.have_falling_block) break; /* "A"-Taster gedrueckt. * Fallenden Block nach rechts rotieren. */ block_rotate(&rotated_block, &tetris.falling_block, 1); if (!check_block_collision(&rotated_block)) { block_lines_set_modified(&tetris.falling_block); block_lines_set_modified(&rotated_block); tetris.falling_block = rotated_block; } break; case BTN_B: if (event->action != BTNACT_PRESSED) break; if (!tetris.have_falling_block) break; /* "B"-Taster gedrueckt. * Fallenden Block nach links rotieren. */ block_rotate(&rotated_block, &tetris.falling_block, -1); if (!check_block_collision(&rotated_block)) { block_lines_set_modified(&tetris.falling_block); block_lines_set_modified(&rotated_block); tetris.falling_block = rotated_block; } break; case NR_BTN: break; } } /** \brief Tasterereignis fuer das Intro verarbeiten. */ static void tetris_intro_handle_button(const struct button_event *event) { switch (event->button) { case BTN_UP: if (event->action != BTNACT_PRESSED) break; /* "up"-Taster gedrueckt. * Lautstaerke erhoehen. */ buzzer_volume_set(buzzer_volume_get() + 1); break; case BTN_LEFT: break; case BTN_RIGHT: break; case BTN_DOWN: if (event->action != BTNACT_PRESSED) break; /* "down"-Taster gedrueckt. * Lautstaerke verringern. */ buzzer_volume_set(buzzer_volume_get() - 1); break; case BTN_A: case BTN_B: if (event->action != BTNACT_PRESSED) break; /* "A"- oder "B"-Taster gedrueckt. * Spiel starten. */ tetris_enter_state_ingame(); break; case NR_BTN: break; } } /** \brief Tasterereignis fuer die Statusanzeige verarbeiten. */ static void tetris_gameover_handle_button(const struct button_event *event) { switch (event->button) { case BTN_UP: case BTN_LEFT: case BTN_RIGHT: case BTN_DOWN: break; case BTN_A: case BTN_B: if (event->action != BTNACT_PRESSED) break; /* "A"- oder "B"-Taster gedrueckt. * Intro anzeigen. */ tetris_enter_state_intro(); break; case NR_BTN: break; } } /** \brief Tasterereignis verarbeiten. */ static void tetris_handle_button(const struct button_event *event) { /* Tasterereignis je nach Spielzustand verarbeiten. */ switch (tetris.state) { case TETRIS_INTRO: tetris_intro_handle_button(event); break; case TETRIS_INGAME: tetris_game_handle_button(event); break; case TETRIS_GAMEOVER: tetris_gameover_handle_button(event); break; } } /** \brief Tasternverzoegerungen fuer autofire. */ static uint8_t tetris_get_autofire_delay(enum button button) { switch (button) { case BTN_LEFT: case BTN_RIGHT: if (tetris.state == TETRIS_INGAME) return TIMER_INTER_MOVE; break; case BTN_UP: case BTN_DOWN: if (tetris.state == TETRIS_INTRO) return TIMER_INTER_VOLUPDOWN; break; case BTN_A: case BTN_B: case NR_BTN: break; } return 0; } /** \brief Periodische 100 Hz Abarbeitung. */ static void tetris_100hz_tick(void) { switch (tetris.state) { case TETRIS_INTRO: break; case TETRIS_INGAME: /* Spiel-Tick dekrementieren und auswerten. */ tetris.timer_count--; if (!tetris.timer_count) { /* Spiel-Tick verarbeiten und ruecksetzen. */ tetris_timer_handle(); if (tetris.fast_down) { tetris.timer_count = TIMER_INTER_FASTDOWN; } else { tetris.timer_count = tetris.timer_inter; } } break; case TETRIS_GAMEOVER: break; } } /** \brief Periodische Abarbeitung. */ static void tetris_work(void) { /* LCD ggf. neuzeichnen. */ tetris_lcd_update(); } /** \brief Tetris initialisieren. */ static void tetris_init(void) { memset(&tetris, 0, sizeof(tetris)); tetris_enter_state_intro(); } /** Schnittstelle zur Tetris Spiellogik anlegen. */ struct game_interface tetris_game_interface = { .handle_button = tetris_handle_button, .get_autofire_delay = tetris_get_autofire_delay, .do_100hz_tick = tetris_100hz_tick, .periodic_work = tetris_work, .init = tetris_init, };