#include #include #include #include #include #include #include #include #include #include // New imports #include #include #include #include #include #include // The game state can be used to detect what happens on the playfield #define GAMEOVER 0 #define ACTIVE (1 << 0) #define ROW_CLEAR (1 << 1) #define TILE_ADDED (1 << 2) // First conversion to split the values of 8 bit RGB, // then some magic bit masking stuff to convert into RGB565. #define RGB(N) { \ .r = (((N & 0xFF0000) >> 16) >> 3) & 0x1f, \ .g = (((N & 0x00FF00) >> 8) >> 2) & 0x3f, \ .b = (((N & 0x0000FF) ) >> 3) & 0x1f, \ } typedef struct { uint8_t r : 5; uint8_t g : 6; uint8_t b : 5; } color_t; // If you extend this structure, either avoid pointers or adjust // the game logic allocate/deallocate and reset the memory typedef struct { bool occupied; // Added a color to every tile. color_t color; } tile; typedef struct { unsigned int x; unsigned int y; } coord; typedef struct { coord const grid; // playfield bounds unsigned long const uSecTickTime; // tick rate unsigned long const rowsPerLevel; // speed up after clearing rows unsigned long const initNextGameTick; // initial value of nextGameTick unsigned int tiles; // number of tiles played unsigned int rows; // number of rows cleared unsigned int score; // game score unsigned int level; // game level tile *rawPlayfield; // pointer to raw memory of the playfield tile **playfield; // This is the play field array unsigned int state; coord activeTile; // current tile unsigned long tick; // incremeted at tickrate, wraps at nextGameTick // when reached 0, next game state calculated unsigned long nextGameTick; // sets when tick is wrapping back to zero // lowers with increasing level, never reaches 0 } gameConfig; gameConfig game = { .grid = {8, 8}, .uSecTickTime = 10000, .rowsPerLevel = 2, .initNextGameTick = 50, }; // This is (supposed to be) monokai. // Because of how the display works, it doesn't look correct on the sense hat, // but at least they are clear and distinct colors. const color_t color_scheme[] = { RGB(0xF92672), // red RGB(0xA6E22E), // green RGB(0xA1EFE4), // cyan RGB(0x66D9EF), // blue RGB(0xAE81FF), // violet RGB(0xFD5FF0), // magenta RGB(0xFD971F), // orange RGB(0xE6DB74), // yellow }; const size_t color_scheme_n = sizeof(color_scheme) / sizeof(color_t); const color_t black = { .r = 0, .g = 0, .b = 0, }; static inline uint16_t color_to_pixel(color_t color) { return color.r | color.g << 5 | color.b << 11; } // The setup of the LED matrix and the joystick was pretty similar, // So I extracted the differences and made this unified procedure which // are common for both of them. static bool initializeDevice(char* dev_path, char* dev_prefix, char* sys_path, char* dev_name, bool (*handle_device)(char*)) { DIR* devdir = opendir(dev_path); if (devdir == NULL) { fprintf(stderr, "Could not open directory '%s'...", dev_path); return false; } struct dirent* file; while ((file = readdir(devdir))) { char* devx = file->d_name; if (strncmp(devx, dev_prefix, strlen(dev_prefix)) != 0) continue; // /sys/... %s dev xx char* name_file = alloca(strlen(sys_path) - 2 + strlen(dev_prefix) + 2); sprintf(name_file, sys_path, devx); // read out the content of /sys/class/...../name, and compare it to the expected name FILE* name_fd = fopen(name_file, "r"); char* content = alloca(strlen(dev_name)); fgets(content, strlen(dev_name) + 1, name_fd); int device_name_is_matching = strncmp(content, dev_name, strlen(dev_name)); fclose(name_fd); if (device_name_is_matching != 0) continue; // If the device name is correct, do something interesting with it. // If that fails, clean up the content of this proc, and forward the failure. if (!handle_device(devx)) goto exitError; return true; } fprintf(stderr, "Could not find device with name %s. Is it plugged in?\n", dev_name); exitError: closedir(devdir); return false; } uint16_t* framebuffer = NULL; struct fb_fix_screeninfo fixed_info; struct fb_var_screeninfo variable_info; int fb_size = sizeof(color_t) * 8 * 8; // Initialization for the led matrix. // This creates a file handler for the framebuffer device, // memory maps it to an array of the same size, // and then closes the file handler. static bool led_matrix_callback(char* fbx) { printf("Found SenseHat framebuffer: %s\n", fbx); char devfbx[9]; sprintf(devfbx, "/dev/%s", fbx); int framebuffer_fd = open(devfbx, O_RDWR); if (framebuffer_fd == -1) { fprintf(stderr, "Could not open SenseHat framebuffer...\n"); return false; } if ( ioctl(framebuffer_fd, FBIOGET_FSCREENINFO, &fixed_info) != 0 || ioctl(framebuffer_fd, FBIOGET_VSCREENINFO, &variable_info) != 0 ) { fprintf(stderr, "Could not get screen info for %s...\n", devfbx); return false; }; // I think this will probably break if it for some reason turns out not to be 16 * 8 * 8, // but I'll leave the line here nonetheless. fb_size = fixed_info.line_length * variable_info.xres * variable_info.yres; printf("Screen size: %i * %i = %i\n", variable_info.xres, variable_info.yres, fb_size); framebuffer = (uint16_t*) mmap(0, fb_size, PROT_WRITE, MAP_SHARED, framebuffer_fd, 0); if (framebuffer == -1) { fprintf(stderr, "Could not create framebuffer mapping...\n"); return false; } close(framebuffer_fd); return true; } int joystick_fd = -1; // Initialization for the joystick // This creates a file handler for the joystick device, // in order for it to be continuously read during the game. static bool joystick_callback(char* eventx) { printf("Found SenseHat joystick: %s\n", eventx); char deveventx[17]; sprintf(deveventx, "/dev/input/%s", eventx); joystick_fd = open(deveventx, O_RDONLY); if (joystick_fd == -1) { fprintf(stderr, "Could not open SenseHat joystick...\n"); return false; } return true; } // This function is called on the start of your application // Here you can initialize what ever you need for your task // return false if something fails, else true static bool initializeSenseHat() { // Set randomness seed to current time, in order for the colors to // become (virtually) different every time you run the program. srand(time(0)); initializeDevice( "/dev", "fb", "/sys/class/graphics/%s/name", "RPi-Sense FB", led_matrix_callback ); initializeDevice( "/dev/input", "event", "/sys/class/input/%s/device/name", "Raspberry Pi Sense HAT Joystick", joystick_callback ); return (framebuffer != NULL && joystick_fd != -1); } // This function is called when the application exits // Here you can free up everything that you might have opened/allocated static void freeSenseHat() { munmap(framebuffer, fb_size); close(joystick_fd); } // This function should return the key that corresponds to the joystick press // KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, with the respective direction // and KEY_ENTER, when the the joystick is pressed // !!! when nothing was pressed you MUST return 0 !!! int readSenseHatJoystick() { struct pollfd poll_fd = { .fd = joystick_fd, .events = POLLIN, }; int new_input_available = poll(&poll_fd, 1, 0); if (!new_input_available) return 0; struct input_event event; read(joystick_fd, &event, sizeof(event)); // If the event value is not 'playing', // it means that the joystick was released back to the center, // which is an event we don't really care about. if (event.value != FF_STATUS_PLAYING) return 0; switch (event.code) { case KEY_UP: case KEY_DOWN: case KEY_LEFT: case KEY_RIGHT: case KEY_ENTER: return event.code; default: return 0; } } // This function should render the gamefield on the LED matrix. It is called // every game tick. The parameter playfieldChanged signals whether the game logic // has changed the playfield static void renderSenseHatMatrix(bool const playfieldChanged) { if (!playfieldChanged) return; for (int i = 0; i < variable_info.xres * variable_info.yres; i++) framebuffer[i] = color_to_pixel(game.rawPlayfield[i].occupied ? game.rawPlayfield[i].color : black); } // The game logic uses only the following functions to interact with the playfield. // if you choose to change the playfield or the tile structure, you might need to // adjust this game logic <> playfield interface static inline void newTile(coord const target) { game.playfield[target.y][target.x].occupied = true; game.playfield[target.y][target.x].color = color_scheme[rand() % color_scheme_n]; } static inline void copyTile(coord const to, coord const from) { memcpy((void *) &game.playfield[to.y][to.x], (void *) &game.playfield[from.y][from.x], sizeof(tile)); } static inline void copyRow(unsigned int const to, unsigned int const from) { memcpy((void *) &game.playfield[to][0], (void *) &game.playfield[from][0], sizeof(tile) * game.grid.x); } static inline void resetTile(coord const target) { memset((void *) &game.playfield[target.y][target.x], 0, sizeof(tile)); } static inline void resetRow(unsigned int const target) { memset((void *) &game.playfield[target][0], 0, sizeof(tile) * game.grid.x); } static inline bool tileOccupied(coord const target) { return game.playfield[target.y][target.x].occupied; } static inline bool rowOccupied(unsigned int const target) { for (unsigned int x = 0; x < game.grid.x; x++) { coord const checkTile = {x, target}; if (!tileOccupied(checkTile)) { return false; } } return true; } static inline void resetPlayfield() { for (unsigned int y = 0; y < game.grid.y; y++) { resetRow(y); } } // Below here comes the game logic. Keep in mind: You are not allowed to change how the game works! // that means no changes are necessary below this line! And if you choose to change something // keep it compatible with what was provided to you! bool addNewTile() { game.activeTile.y = 0; game.activeTile.x = (game.grid.x - 1) / 2; if (tileOccupied(game.activeTile)) return false; newTile(game.activeTile); return true; } bool moveRight() { coord const newTile = {game.activeTile.x + 1, game.activeTile.y}; if (game.activeTile.x < (game.grid.x - 1) && !tileOccupied(newTile)) { copyTile(newTile, game.activeTile); resetTile(game.activeTile); game.activeTile = newTile; return true; } return false; } bool moveLeft() { coord const newTile = {game.activeTile.x - 1, game.activeTile.y}; if (game.activeTile.x > 0 && !tileOccupied(newTile)) { copyTile(newTile, game.activeTile); resetTile(game.activeTile); game.activeTile = newTile; return true; } return false; } bool moveDown() { coord const newTile = {game.activeTile.x, game.activeTile.y + 1}; if (game.activeTile.y < (game.grid.y - 1) && !tileOccupied(newTile)) { copyTile(newTile, game.activeTile); resetTile(game.activeTile); game.activeTile = newTile; return true; } return false; } bool clearRow() { if (rowOccupied(game.grid.y - 1)) { for (unsigned int y = game.grid.y - 1; y > 0; y--) { copyRow(y, y - 1); } resetRow(0); return true; } return false; } void advanceLevel() { game.level++; switch(game.nextGameTick) { case 1: break; case 2 ... 10: game.nextGameTick--; break; case 11 ... 20: game.nextGameTick -= 2; break; default: game.nextGameTick -= 10; } } void newGame() { game.state = ACTIVE; game.tiles = 0; game.rows = 0; game.score = 0; game.tick = 0; game.level = 0; resetPlayfield(); } void gameOver() { game.state = GAMEOVER; game.nextGameTick = game.initNextGameTick; } bool sTetris(int const key) { bool playfieldChanged = false; if (game.state & ACTIVE) { // Move the current tile if (key) { playfieldChanged = true; switch(key) { case KEY_LEFT: moveLeft(); break; case KEY_RIGHT: moveRight(); break; case KEY_DOWN: while (moveDown()) {}; game.tick = 0; break; default: playfieldChanged = false; } } // If we have reached a tick to update the game if (game.tick == 0) { // We communicate the row clear and tile add over the game state // clear these bits if they were set before game.state &= ~(ROW_CLEAR | TILE_ADDED); playfieldChanged = true; // Clear row if possible if (clearRow()) { game.state |= ROW_CLEAR; game.rows++; game.score += game.level + 1; if ((game.rows % game.rowsPerLevel) == 0) { advanceLevel(); } } // if there is no current tile or we cannot move it down, // add a new one. If not possible, game over. if (!tileOccupied(game.activeTile) || !moveDown()) { if (addNewTile()) { game.state |= TILE_ADDED; game.tiles++; } else { gameOver(); } } } } // Press any key to start a new game if ((game.state == GAMEOVER) && key) { playfieldChanged = true; newGame(); addNewTile(); game.state |= TILE_ADDED; game.tiles++; } return playfieldChanged; } int readKeyboard() { struct pollfd pollStdin = { .fd = STDIN_FILENO, .events = POLLIN }; int lkey = 0; if (poll(&pollStdin, 1, 0)) { lkey = fgetc(stdin); if (lkey != 27) goto exit; lkey = fgetc(stdin); if (lkey != 91) goto exit; lkey = fgetc(stdin); } exit: switch (lkey) { case 10: return KEY_ENTER; case 65: return KEY_UP; case 66: return KEY_DOWN; case 67: return KEY_RIGHT; case 68: return KEY_LEFT; } return 0; } void renderConsole(bool const playfieldChanged) { if (!playfieldChanged) return; // Goto beginning of console fprintf(stdout, "\033[%d;%dH", 0, 0); for (unsigned int x = 0; x < game.grid.x + 2; x ++) { fprintf(stdout, "-"); } fprintf(stdout, "\n"); for (unsigned int y = 0; y < game.grid.y; y++) { fprintf(stdout, "|"); for (unsigned int x = 0; x < game.grid.x; x++) { coord const checkTile = {x, y}; fprintf(stdout, "%c", (tileOccupied(checkTile)) ? '#' : ' '); } switch (y) { case 0: fprintf(stdout, "| Tiles: %10u\n", game.tiles); break; case 1: fprintf(stdout, "| Rows: %10u\n", game.rows); break; case 2: fprintf(stdout, "| Score: %10u\n", game.score); break; case 4: fprintf(stdout, "| Level: %10u\n", game.level); break; case 7: fprintf(stdout, "| %17s\n", (game.state == GAMEOVER) ? "Game Over" : ""); break; default: fprintf(stdout, "|\n"); } } for (unsigned int x = 0; x < game.grid.x + 2; x++) { fprintf(stdout, "-"); } fflush(stdout); } inline unsigned long uSecFromTimespec(struct timespec const ts) { return ((ts.tv_sec * 1000000) + (ts.tv_nsec / 1000)); } int main(int argc, char **argv) { (void) argc; (void) argv; // This sets the stdin in a special state where each // keyboard press is directly flushed to the stdin and additionally // not outputted to the stdout { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag &= ~(ICANON | ECHO); ttystate.c_cc[VMIN] = 1; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } // Allocate the playing field structure game.rawPlayfield = (tile *) malloc(game.grid.x * game.grid.y * sizeof(tile)); game.playfield = (tile**) malloc(game.grid.y * sizeof(tile *)); if (!game.playfield || !game.rawPlayfield) { fprintf(stderr, "ERROR: could not allocate playfield\n"); return 1; } for (unsigned int y = 0; y < game.grid.y; y++) { game.playfield[y] = &(game.rawPlayfield[y * game.grid.x]); } // Reset playfield to make it empty resetPlayfield(); // Start with gameOver gameOver(); if (!initializeSenseHat()) { fprintf(stderr, "ERROR: could not initilize sense hat\n"); return 1; }; // Clear console, render first time fprintf(stdout, "\033[H\033[J"); renderConsole(true); renderSenseHatMatrix(true); while (true) { struct timeval sTv, eTv; gettimeofday(&sTv, NULL); int key = readSenseHatJoystick(); if (!key) key = readKeyboard(); if (key == KEY_ENTER) break; bool playfieldChanged = sTetris(key); renderConsole(playfieldChanged); renderSenseHatMatrix(playfieldChanged); // Wait for next tick gettimeofday(&eTv, NULL); unsigned long const uSecProcessTime = ((eTv.tv_sec * 1000000) + eTv.tv_usec) - ((sTv.tv_sec * 1000000 + sTv.tv_usec)); if (uSecProcessTime < game.uSecTickTime) { usleep(game.uSecTickTime - uSecProcessTime); } game.tick = (game.tick + 1) % game.nextGameTick; } freeSenseHat(); free(game.playfield); free(game.rawPlayfield); return 0; }