Skip to content

Commit

Permalink
Readme updated, Ask on quit added to driver
Browse files Browse the repository at this point in the history
* Readme updated with more detailed docs
* Game driver asks if you're sure you want to quit without exiting
* Game driver tested with various different board sizes (45x20,
  16x32,50x50, ...). All worked!
* DEBUG_T added as Compilation macro to tetris/CMakeLists.txt so it can
  be enabled/disabled at build time and builds tested with it disabled.
  Any prints that weren't gated before are now.
* CI updated so runs include DEBUG_T flag
  • Loading branch information
0xjmux committed Mar 27, 2024
1 parent 31afd27 commit 0305e2a
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 43 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
run: cmake -B ${{github.workspace}}/build -DTARGET_GROUP=test
-DTETRIS_UNIT_TEST_CI=ON
-DTETRIS_PRINT_BOARD_IN_TESTS_MACRO=ON
-DTETRIS_DEBUG_T_MACRO=ON
# run: cmake -B ${{github.workspace}}/build -DTARGET_GROUP=test -DTETRIS_UNIT_TEST_MACRO=ON
# build cmake in build dir with CI unit test flag turned on - disable macro bc causes some tests to not run

Expand Down
51 changes: 38 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

This game engine is part of a larger project where I'm implementing a version of Tetris playable on WS2812B LED matrices via an ESP-Now remote.

The game was branched off to it's own repository because the embedded project source tree only needs `tetris.h` and `tetris.c`, but all the other driver and testing files take up a decent amount of space - so while i'm developing it, it'll live here.

> [!WARNING]
> This project is still in active development, bugs are still being found. When this is no longer the case, this notice will be removed.
The game was branched off to its own repository because the embedded project source tree only needs `tetris.h` and `tetris.c`, but all the other driver and testing files take up a lot of space.

### TODO:
* [ ] fix duplicate colors
* this is an ncurses color issue, the game is rendering it fine.
* [ ] fix moving left/right choppy feel
* [ ] add confirmation on quit
* this is an ncurses color issue with TERM, the game logic is setting it correctly
* [ ] maybe rewrite render_active_board to no-copy to increase speed?

#### fixed
Expand All @@ -27,6 +22,7 @@ The game was branched off to it's own repository because the embedded project so
* [X] add play/pause functionality
* [X] Finish tetris game logic
* [X] fix gameover state not detected
* [X] add confirmation on quit to driver



Expand Down Expand Up @@ -56,18 +52,47 @@ mostly for myself to not forget
### Compilation
#### Game Driver (runs in terminal on Linux workstations)
```sh
cmake . -B build && cmake --build build'
cmake . -B build && cmake --build build
./build/tetris_driver
```

#### Unit Tests
```sh
cmake . -B build -DTARGET_GROUP=test && cmake --build build'
cmake . -B build -DTARGET_GROUP=test && cmake --build build
./build/test_tetris
```

#### Linux Game Controls
* For your own microcontroller implementation, see `src/driver_tetris.c` as an example; it shows you everything that's needed for a function implementation. The game is designed to be as self-contained as possible, so past passing along inputs and rendering the board array there's not much you need to do.
#### Debugging
If debugging flags are enabled, two game state files will show up in the current directory when the game is finished: `game.log` and `final-gamestate.ini`. The `game.log` is a log of actions taken during the game to speed up tracing logic problems; `final-gamestate.ini` contains a human & machine readable save of the entire game state at gameover, allowing easier debugging of premature exit conditions (which was one of the bigger bugs I had to find). The log file automatically updates during gameplay, so a live log of what's happening in-game can be watched in a separate terminal session by doing `tail -f game.log`.

One of the main reasons I set up the `.ini` file saving functionality was for unit testing. This can be seen in the `test_clearRowsDumpedGame()` functions inside `test/suite_1.c`. The game state is restored and then used to test edge cases and look for weird behavior, all starting from an actual state reached in-game.


#### Implementation and Controls
* For your own microcontroller implementation, see `src/driver_tetris.c` as an example; it shows you everything that's needed for a complete implementation. The game is designed to be as self-contained as possible, so past passing along inputs and rendering the board array there's not much you need to do.

Essentially, this is it:
```c
// start game, define player_move variable, and create first piece
TetrisGame *tg = create_game();
enum player_move move = T_NONE;
create_rand_piece(tg);

while([game not over]) {
// do game tick
tg_tick(tg, move);

// process player input
// wait for 10 ms before doing it again
}
```
The game board itself is rendered to a 2D array `int8_t board[TETRIS_ROWS][TETRIS_COLS]` accessible via `tg->active_board.board`. All your display implementation needs to do is render this array into the associated colors for whatever display format is desired.
The code is documented using Doxygen style comments. Custom types are documented in `tetris.h`, and functions are preceded by short explanations in `tetris.c`. On inclusion into your project, your IDE's LSP server should automatically show these descriptions on hover.
##### Linux Game Controls
These are the controls for my implementation (`driver_tetris.c`) for POSIX terminals via ncurses.
```
Use arrow keys to move
SPACE pauses game
Expand All @@ -77,11 +102,11 @@ SPACE pauses game
#### Flags
* There are many compilation flags to enable/disable features, usually for debugging purposes. I've also created several compilation flags that enable extra output on CI builds so it's easier to see what went wrong from the build report console. The default options should be fine, but for finer tuning you can see the options available to you across the project's `CMakeLists.txt` files.
* There are many compilation flags to enable/disable features, mostly for debugging. I've also created several compilation flags that enable extra output on CI builds so it's easier to see what went wrong from the build report console. The default options should be fine, but for finer tuning you can see the options available to you across the project's `CMakeLists.txt` files.
##### Other
I have these in my `~/.zshrc` to make the testing process more fluid.
I have these aliases in my `~/.zshrc` to make the testing process more fluid.
```sh
alias cmakeclean='rm -rf CMakeCache.txt cmake_install.cmake Makefile CMakeFiles build'
alias cmakeregen='cmake . -B build && cmake --build build'
Expand Down
49 changes: 34 additions & 15 deletions src/driver_tetris.c
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* This file acts as a driver for the tetris game so it can be run on
* an x86 machine to test functionality before porting to RTOS
* @file
* @brief
* @version
* @author
* @date
* @file driver_tetris.c
* @brief Driver for tetris game so it can be run on Linux workstations for testing
* @version 1.0
* @author Jacob Bokor
* @date 03/2024
*
*/

Expand Down Expand Up @@ -63,7 +63,6 @@ int main(void) {


tg = create_game();
// tg->board = create_board();
enum player_move move = T_NONE;
create_rand_piece(tg); // create first piece

Expand Down Expand Up @@ -114,15 +113,36 @@ int main(void) {

break;

// Quit game
case 'q':
move = T_QUIT;
wclear(g_win);
box(g_win,0,0);
wmove(g_win, (TETRIS_ROWS / 10), (TETRIS_COLS * BLOCK_WIDTH / 2) -4);
wprintw(g_win, "QUIT? [Yy/Nn]");
wrefresh(g_win);
// change getch() back to blocking so pause holds until resumed
timeout(-1);
char response = getch();

if (response == 'y' || response == 'Y' || \
response == ' ' || response == 'q') {
move = T_QUIT;
break;
}
else {
move = T_NONE;
}

timeout(0);
break;
// save game to disk
case 'p':
move = T_NONE;
save_game_state(tg, "gamestate.ini");
mvwprintw(s_win, 4,1, "GAME STATE SAVED\n");
#ifdef DEBUG_T
fprintf(gamelog, "game state saved to file gamestate.ini\n");
#endif
wnoutrefresh(s_win);

break;
Expand All @@ -147,9 +167,6 @@ int main(void) {
#endif


// TODO ask player if they want to play again


// if we're here, game is over; dealloc tg
end_game(tg);
endwin();
Expand Down Expand Up @@ -225,14 +242,16 @@ void update_score(WINDOW *w, TetrisGame *tg) {

/**
* Debug function to print out player move
* Only works when DEBUG_T def enabled
*/
void print_keypress(enum player_move move) {
// translate enum value into human-readable move
char const* move_str[] = {"T_NONE", "T_UP", "T_DOWN", "T_LEFT", \
"T_RIGHT", "T_PLAYPAUSE", "T_QUIT"};
fprintf(gamelog, "Received move: %s\n", move_str[move]);
fflush(gamelog);

#ifdef DEBUG_T
char const* move_str[] = {"T_NONE", "T_UP", "T_DOWN", "T_LEFT", \
"T_RIGHT", "T_PLAYPAUSE", "T_QUIT"};
fprintf(gamelog, "Received move: %s\n", move_str[move]);
fflush(gamelog);
#endif
}


Expand Down
28 changes: 18 additions & 10 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ const char* get_piece_str(enum piece_type ptype) {
* Simple function to create a new piece with the specified parameters
*/
TetrisPiece create_tetris_piece(enum piece_type ptype, \
int16_t row, int16_t col, uint8_t orientation) {
int16_t row, int16_t col, uint8_t orientation)
{
assert(orientation >= 0 && orientation < 4 && "Orientation out of range");

TetrisPiece new_piece = {.ptype = ptype, .orientation = orientation, \
.loc.col = col, .loc.row = row , .falling=true};

return new_piece;
}



// macros for inih parsing
// macros for ini parsing
#define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0
#define MATCH_SECTION(n) strcmp(section, n) == 0
#define MATCH_KEY(n) strcmp(name, n) == 0
Expand All @@ -43,7 +46,6 @@ static int handler(void* user, const char* section, const char* name,
TetrisGame *tg = (TetrisGame*)user;

// this is ugly but ig you cant do it with switch/case so here we go
//MATCH("SECTION", "KEY")
if (MATCH_SECTION("TETRIS_GAME_STRUCT")) {

if (MATCH_KEY("game_over")) {
Expand Down Expand Up @@ -94,8 +96,6 @@ static int handler(void* user, const char* section, const char* name,

// read board from file
else if (MATCH_SECTION("active_board")) {

// not the most efficient way of going about this, but it's cleaner at least
reconstruct_board_from_str_row(&tg->active_board, name, value);

}
Expand Down Expand Up @@ -151,7 +151,7 @@ void reconstruct_board_from_str_row(TetrisBoard *tb, const char *name, const cha
strcpy(str_row, value);


// this is not a great way to do this but it'll only ever run on
// there are better ways of doing this but it'll only ever run on
// "normal" computers and should be good enough
for (int i = 0; i < TETRIS_ROWS; i++) {
snprintf(curr_row, MAX_ROW_NAME_LEN, "row_%d", i);
Expand All @@ -166,7 +166,6 @@ void reconstruct_board_from_str_row(TetrisBoard *tb, const char *name, const cha
tb->board[i][j] = atoi(curr_cell);
}

// printf("copied row %d: %s\n", i, value);
}
}

Expand Down Expand Up @@ -246,12 +245,21 @@ void ini_save_board_to_file(FILE *file, TetrisBoard tb) {
/**
* Print current board state to console
* @param TetrisBoard
* @param *file to print to - NULL for default
* @param *file to print to - NULL for default (gamelog)
* @param ini_out - if printing to an .ini file, prepend each line with comment char ";"
*/
void print_board_state(TetrisBoard tb, FILE *file, bool ini_out) {
if (file == NULL)
file = gamelog;
if (file == NULL) {
#ifdef DEBUG_T
file = gamelog;
#else
// if this is a debug statement but debug is disabled, exit now
file = NULL;
return;
#endif
}


// draw existing pieces on board
if (ini_out) fprintf(file, "; ");
fprintf(file, "Highest occupied cell: %d\n", tb.highest_occupied_cell);
Expand Down
7 changes: 7 additions & 0 deletions tetris/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ target_include_directories(tetris PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)

##### OPTIONAL DEBUG FLAG ######
# This flag gates all print statements inside tetris.c. Extremely helpful for debugging,
# but shoudln't be present in release builds
OPTION(TETRIS_DEBUG_T_MACRO "Enable Debug logging from inside tetris" OFF)
IF(TETRIS_DEBUG_T_MACRO)
target_compile_definitions(tetris PUBLIC DEBUG_T=1)
ENDIF(TETRIS_DEBUG_T_MACRO)

# include_directories(${PROJECT_SOURCE_DIR})
8 changes: 5 additions & 3 deletions tetris/tetris.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ TetrisGame* create_game(void) {


#ifdef DEBUG_T

#ifndef TETRIS_UNIT_TEST_DEF
// separate gamelog file to prevent ncurses printing issues
gamelog = fopen("game.log", "w+");
#else
// for unit testing, assign game.log to stdout
// for unit testing, assign gamelog to stdout so the output shows up
// in the GH actions console
gamelog = stdout;
#endif
#endif
Expand Down Expand Up @@ -105,7 +105,9 @@ bool tg_tick(TetrisGame *tg, enum player_move move) {
check_and_spawn_new_piece(tg); // includes row clearing and score updates
render_active_board(tg);
if (check_game_over(tg)) { // not fully implemented yet
fprintf(gamelog, "game over detected, returning false from tg_tick\n");
#ifdef DEBUG_T
fprintf(gamelog, "game over detected, returning false from tg_tick\n");
#endif
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions tetris/tetris.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

// TETRIS GAME LOGIC DEBUG FLAG
// all print/fprintf statements in tetris.c are gated by this flag. extremely helpful
// for debugging, but will need to be disabled on MCU platforms
#define DEBUG_T 1
// for debugging game logic, but should not be present in release builds
// #define DEBUG_T 1

#ifdef DEBUG_T
// separate game log file to prevent ncurses printing issues
Expand Down

0 comments on commit 0305e2a

Please sign in to comment.