638 lines
17 KiB
C
638 lines
17 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <termios.h>
|
|
#include <sys/select.h>
|
|
#include <linux/input.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <poll.h>
|
|
|
|
// New imports
|
|
#include <linux/fb.h>
|
|
#include <dirent.h>
|
|
#include <fcntl.h>
|
|
#include <stdint.h>
|
|
#include <sys/mman.h>
|
|
#include <time.h>
|
|
|
|
// 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;
|
|
}
|