TDT4258/ex3/stetris.c

638 lines
17 KiB
C
Raw Permalink Normal View History

2022-12-19 18:06:38 +01:00
#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;
}