diff --git a/README.md b/README.md index e549eb9..6102d39 100644 --- a/README.md +++ b/README.md @@ -4,50 +4,29 @@ This game engine is part of a larger project where I'm implementing a version of 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 with TERM, the game logic is setting it correctly -* [ ] maybe rewrite render_active_board to no-copy to increase speed? - -#### fixed -* [X] fix J piece rotation -* [X] Set up CI/CD with GH actions -* [X] fix full lines not being removed -* [X] fix crash on trying to clear line -* [X] fix gameover state detected prematurely, causing crash [Issue #1](https://github.com/0xjmux/tetris/issues/1) -* [X] fix score not incrementing on line clear -* [X] maybe fixed: figure out why check_and_clear being called on rows at very top of board - * this preceeds premature gameover, this crash seems to happen on another piece clear -* [X] figure out what's causing a bunch of out-of-bounds numbers to show up in row 1 of board - i think this is a type issue with uint & int types! -* [X] add play/pause functionality -* [X] Finish tetris game logic -* [X] fix gameover state not detected -* [X] add confirmation on quit to driver - - ### About * One of the main goals of this project is to gain experience developing and debugging components on a workstation that can then be easily ported to MCU platforms. As such, the linux driver for this game is filled with debugging features to make hunting bugs in the game logic as easy as possible, since that same process will be much more difficult when in the resource-constrained environment of an MCU. -* Some features include +* Some debugging features include * Saving entire game state to a file at any point by pressing `p`. Game state can then be restored, but these saves are largely used for unit testing purposes since having a start point of a game that you know preceded a crash can be very helpful when trying to force the game into invalid states. * Some of these files can be found in [test/files/](./test/files/) * Game state is also saved to a file on gameover, which was done to debug issues that cause premature gameover conditions. + * Debug sub-window in game enableable via `-DDEBUG_WINDOW=ON` during cmake build. This shows current game state information, so weird behavior is easier to spot. * Everything is written from scratch, with the exception of the graphics library (ncurses), Unit test harness (Unity), and `.ini` file parser, inih. -* ### Project Goals This project has several goals. -* Game needs to be runnable on low-powered devices using interrupts +* Game needs to be runnable on low-powered devices using interrupts. Memory and compute requirements should be as minimal as possible. * Driver code needed to make a given display or controller work should be kept as minimal as possible. Currently, the game exposes a 2D array of `int8_t` values representing different piece colors, and all the display implementation needs to do is render that to a grid of the board size. -* Teach myself embedded test-driven development using techniques from James Greening's "Test-Driven Development for Embedded C" ([link](https://pragprog.com/titles/jgade/test-driven-development-for-embedded-c/)) +* Teach myself embedded test-driven development and embedded unit testing using techniques from James Greening's "Test-Driven Development for Embedded C" ([link](https://pragprog.com/titles/jgade/test-driven-development-for-embedded-c/)) +* Set up CI from scratch for an embedded project +* Learn CMake and set up the build system from scratch + This game is my first attempt at writing a component of medium complexity, and one of my primary goals is bettering my understanding of development best practices to accelerate my work on future projects. -### Some of what I've learned -mostly for myself to not forget -* CI build options in CMake, so that CI can build a different version depending on paths and such ### Compilation #### Game Driver (runs in terminal on Linux workstations) @@ -94,10 +73,12 @@ The code is documented using Doxygen style comments. Custom types are documented ##### 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 -'q' quits -'p' prints current game state to .ini file (for debugging purposes) +←/→ - Move left/right +↑ - Rotate piece +↓ - Move piece down +SPACE - pause game +'q' - quit +'p' prints current game state to `gamestate.ini` (for debugging purposes) ``` @@ -105,6 +86,20 @@ SPACE pauses game * 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. +Options: +```cmake +OPTION(TETRIS_UNIT_TEST_MACRO "Print gamelog to stdout (for CI)" OFF) # disabled by default +OPTION(TETRIS_UNIT_TEST_CI "CI-specific path options" OFF) # disabled by default +OPTION(TETRIS_DEBUG_T_MACRO "Enable Debug logging from inside tetris" OFF) +OPTION(INI_LIB_INCLUDE_OPTION "Include inih library for saving game state to disk" ON) +``` + +For example, to enable `DEBUG_T` you would do +```sh +cmake -B build -DTARGET_GROUP=test -DTETRIS_DEBUG_T_MACRO=ON +``` + + ##### Other I have these aliases in my `~/.zshrc` to make the testing process more fluid. ```sh @@ -113,3 +108,31 @@ alias cmakeregen='cmake . -B build && cmake --build build' alias cmaketest='cmake . -B build -DTARGET_GROUP=test && cmake --build build' ``` + +--- + +### TODO: +* [ ] fix duplicate colors + * 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 +* [X] fix J piece rotation +* [X] Set up CI/CD with GH actions +* [X] fix full lines not being removed +* [X] fix crash on trying to clear line +* [X] fix gameover state detected prematurely, causing crash [Issue #1](https://github.com/0xjmux/tetris/issues/1) +* [X] fix score not incrementing on line clear +* [X] maybe fixed: figure out why check_and_clear being called on rows at very top of board + * this preceeds premature gameover, this crash seems to happen on another piece clear +* [X] figure out what's causing a bunch of out-of-bounds numbers to show up in row 1 of board - i think this is a type issue with uint & int types! +* [X] add play/pause functionality +* [X] Finish tetris game logic +* [X] fix gameover state not detected +* [X] add confirmation on quit to driver + + +#### Some of what I've learned +mostly for myself to not forget +* CI build options in CMake, so that CI can build a different version depending on paths and such +* static variables inside functions can be useful, but generally make unit testing more difficult. \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0d60acb..b67f52b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,7 +14,7 @@ target_include_directories(tetris_driver PUBLIC ${PROJECT_SOURCE_DIR}/include) find_library(NCURSES_FOUND ncurses REQUIRED) -OPTION(DEBUG_WINDOW "Enable window with game state information for debugging" ON) +OPTION(DEBUG_WINDOW "Enable debug window with game state information in ncurses" OFF) IF(DEBUG_WINDOW) target_compile_definitions(tetris_driver PUBLIC DEBUG_T_WIN=1) ENDIF() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6e742df..4af3661 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,8 +31,6 @@ IF(TETRIS_UNIT_TEST_MACRO) ENDIF(TETRIS_UNIT_TEST_MACRO) ##### END OPTIONAL BUILD FLAGS ###### - - target_link_libraries(test_tetris tetris Unity diff --git a/test/suite_1.c b/test/suite_1.c index 1784525..ec874fd 100644 --- a/test/suite_1.c +++ b/test/suite_1.c @@ -415,42 +415,34 @@ void test_getElapsedUs(void) { */ void test_arr_helpers(void) { // test int16_to_uint8_arr() - int16_t arr1[5] = {5,2,1,4,3}; - uint8_t out_arr1[5]; - uint8_t exp_arr1[5] = {5,2,1,4,3}; - int16_to_uint8_arr(arr1, out_arr1, 5); - TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(exp_arr1, \ - out_arr1, 5, "Array conversion to uint8 incorrect"); - - TEST_ASSERT_EQUAL_UINT8_MESSAGE(1, (uint8_t)smallest_in_arr(arr1, 5), \ + uint8_t arr1[5] = {5,2,1,4,3}; + + TEST_ASSERT_EQUAL_UINT8_MESSAGE(1, smallest_in_arr(arr1, 5), \ "smallest_in_arr failed on all positive values"); - int16_t arr2[6] = {INT8_MAX,1,2,255,4,0}; - uint8_t out_arr2[6]; - uint8_t exp_arr2[6] = {INT8_MAX,1,2,255,4,0}; - int16_to_uint8_arr(arr2, out_arr2, 6); - TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(exp_arr2, \ - out_arr2, 6, "arr2 conversion to uint8 incorrect"); + uint8_t arr2[6] = {UINT8_MAX,1,2,255,4,0}; - TEST_ASSERT_EQUAL_UINT8_MESSAGE(0, (uint8_t)smallest_in_arr(arr2, 6), \ + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0, smallest_in_arr(arr2, 6), \ "smallest_in_arr failed on all positive values"); // test smallest in arr - int16_t arr4[5] = {5,2,1,4,3}; - TEST_ASSERT_EQUAL_INT16(1, smallest_in_arr(arr4, 5)); + uint8_t arr4[5] = {5,2,1,4,3}; + TEST_ASSERT_EQUAL_UINT8(1, smallest_in_arr(arr4, 5)); - int16_t arr6[6] = {INT16_MAX,-1,2,-2,4,5}; - TEST_ASSERT_EQUAL_INT16(-2, smallest_in_arr(arr6, 6)); + uint8_t arr6[6] = {UINT8_MAX,1,2,3,4,5}; + TEST_ASSERT_EQUAL_UINT8(1, smallest_in_arr(arr6, 6)); #define ARR7_SIZE 6 uint8_t arr7_uint[ARR7_SIZE] = {2,UINT8_MAX,2,255,0,5}; - int16_t arr7_int16[ARR7_SIZE]; - int16_t arr7_exp[ARR7_SIZE] = {2,UINT8_MAX,2,255,0,5}; - uint8_to_int16_arr(arr7_uint, arr7_int16, ARR7_SIZE); - TEST_ASSERT_EQUAL_INT16_ARRAY_MESSAGE(arr7_int16, \ - arr7_exp, ARR7_SIZE, "arr7 conversion to int16 incorrect"); - TEST_ASSERT_EQUAL_INT16(0, smallest_in_arr(arr7_int16, ARR7_SIZE)); + /* + // int16_t arr7_int16[ARR7_SIZE]; + // int16_t arr7_exp[ARR7_SIZE] = {2,UINT8_MAX,2,255,0,5}; + // uint8_to_int16_arr(arr7_uint, arr7_int16, ARR7_SIZE); + // TEST_ASSERT_EQUAL_INT16_ARRAY_MESSAGE(arr7_int16, \ + // arr7_exp, ARR7_SIZE, "arr7 conversion to int16 incorrect"); + */ + TEST_ASSERT_EQUAL_UINT8(0, smallest_in_arr(arr7_uint, ARR7_SIZE)); // test val_in_arr diff --git a/tetris/tetris.c b/tetris/tetris.c index c756464..162a187 100644 --- a/tetris/tetris.c +++ b/tetris/tetris.c @@ -88,23 +88,20 @@ void end_game(TetrisGame *tg) { } /** - * Process a single Tetris game tick - * This function is the only one you need to call to use the tetris game - everything + * Process a single Tetris game tick. + * @brief This function is the only one you need to call to use the tetris game - everything * is processed internally. Pass player moves into this function, and then just * render the tg->active_board array to your desired display format. - * @param TetrisGame *tg - * @param player_move move + * @param TetrisGame* tg - pointer to TetrisGame struct + * @param player_move most recent player move as enum * @returns true if game is still going, false when game_over */ bool tg_tick(TetrisGame *tg, enum player_move move) { - // handle gravity, input, cleared lines, adjusting score, checking game over - - check_do_piece_gravity(tg); 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 + if (check_game_over(tg)) { // check for game over condition #ifdef DEBUG_T fprintf(gamelog, "game over detected, returning false from tg_tick\n"); #endif @@ -117,7 +114,6 @@ bool tg_tick(TetrisGame *tg, enum player_move move) { case T_UP: if(test_piece_rotate(&tg->board, tg->active_piece)) - // fprintf(gamelog, "piece rotated (not impl yet)\n"); tg->active_piece.orientation = (tg->active_piece.orientation + 1) % 4; break; @@ -156,9 +152,10 @@ bool tg_tick(TetrisGame *tg, enum player_move move) { /** - * combine active_piece and existing board stack into active_board, + * Combine active_piece and existing board stack into active_board, * which is then used by display system to display the actual game. - * This function assumes placement of *tp in board is valid + * + * @note This function assumes placement of *tp in board is valid * @returns active_board, but also updates ptr tg->active_board */ TetrisBoard render_active_board(TetrisGame *tg) { @@ -200,8 +197,8 @@ TetrisBoard render_active_board(TetrisGame *tg) { } /** - * Updates game score based on # lines cleared, along with - * level and gravity tick rate + * Updates game score based on # lines cleared, including + * level and gravity tick rate. * Every 10 lines cleared, level increases by 1 */ void tg_update_score(TetrisGame *tg, uint8_t lines_cleared) { @@ -222,7 +219,6 @@ void tg_update_score(TetrisGame *tg, uint8_t lines_cleared) { // when the level increases, the gravity tick speeds up if it's still above the floor if (tg->gravity_tick_rate_usec > GRAVITY_TICK_RATE_FLOOR) { - // i haven't tested this lower bound but i assume it'll be very difficult tg->gravity_tick_rate_usec -= GRAVITY_TICK_RATE_DELTA; } assert(tg->gravity_tick_rate_usec > GRAVITY_TICK_RATE_FLOOR && \ @@ -251,7 +247,6 @@ TetrisPiece create_rand_piece(TetrisGame *tg) { // create new piece and place in middle center TetrisPiece new_piece; - new_piece.ptype = rand() % NUM_TETROMINOS; new_piece.orientation = 0; new_piece.loc.col = TETRIS_COLS / 2; @@ -388,7 +383,7 @@ bool test_piece_rotate(TetrisBoard *tb, const TetrisPiece tp) { return false; } - // since we would've returned false if we failed earlier, just ret true now + // if we're here, rotation is valid return true; } @@ -404,8 +399,6 @@ bool test_piece_rotate(TetrisBoard *tb, const TetrisPiece tp) { * that this function will need to be modified when porting to other platforms */ bool check_do_piece_gravity(TetrisGame *tg) { - // if time has passed tick interval - // get curr system time struct timeval curr_time_usec; gettimeofday(&curr_time_usec, NULL); @@ -442,9 +435,8 @@ bool check_do_piece_gravity(TetrisGame *tg) { /** - * On placing a piece down, the rows where it landed - * need to be checked to see if they're full so they can - * be cleared and the score can be increased + * Check if `row` is completely filled. + * @returns true if yes, false if no */ bool check_filled_row(TetrisGame *tg, const uint8_t row) { @@ -476,14 +468,12 @@ void clear_rows(TetrisGame *tg, uint8_t top_row, uint8_t num_rows) { for (int row = top_row + num_rows - 1; row - num_rows > 0; row--) { // get next row and move it down tg->board.board[row][col] = tg->board.board[row-num_rows][col]; - // fprintf(stdout, "Clearing row %d normally\n", row); } // clear rows at the very top of the board, where we want to avoid // reading garbage from invalid board locations for (int row = num_rows; row > 0; row--) { assert(row < TETRIS_ROWS); tg->board.board[row][col] = BG_COLOR; - // fprintf(stdout, "Clearing row=%d at top of board\n", row); } } @@ -505,15 +495,14 @@ uint8_t check_and_clear_rows(TetrisGame *tg, tetris_location *tp_cells) { * not doing dupliate work was complex enough that this is prob better. * * in short, this only checks rows where piece ended up, but will - * check all 4 rows; which might mean checking the same row twice + * check the row of every cell in the piece; + * which might mean checking the same row twice */ uint8_t rows_to_clear[4]; uint8_t rows_idx = 0; // index (and size) of rows_to_clear uint8_t row_with_offset; uint8_t piece_max_row = TETRIS_ROWS; for(int i = 0; i < NUM_CELLS_IN_TETROMINO; i++) { - // I THINK SOMETHING IS WRONG SOMEWHERE HERE - row_with_offset = (uint8_t) tp_cells[i].row; assert(row_with_offset < TETRIS_ROWS && row_with_offset >= 0 && "global row out of bounds"); @@ -551,10 +540,11 @@ uint8_t check_and_clear_rows(TetrisGame *tg, tetris_location *tp_cells) { // if we have rows to clear: if (rows_idx > 0) { // convert rows_to_clear to int16 to prevent issues - int16_t rows_to_clear_int16[4]; - uint8_to_int16_arr(rows_to_clear, rows_to_clear_int16, rows_idx ); + // int16_t rows_to_clear_int16[4]; + // uint8_to_int16_arr(rows_to_clear, rows_to_clear_int16, rows_idx ); // find top row in rows_to_clear and clear rows - uint8_t top_row = (uint8_t) smallest_in_arr(rows_to_clear_int16, rows_idx); + uint8_t top_row = smallest_in_arr(rows_to_clear, rows_idx); + // uint8_t top_row = (uint8_t) smallest_in_arr(rows_to_clear_int16, rows_idx); #ifdef DEBUG_T fprintf(gamelog, "clearing %d rows with top_row=%d\n", rows_idx, top_row); fflush(gamelog); @@ -652,10 +642,10 @@ inline bool val_in_arr(const uint8_t val, uint8_t arr[], const uint8_t arr_len) /** * Simple helper function to return smallest value in array - * (int16_t only, used for row clearing operation) + * (int8_t only, used for row clearing operation) */ -int16_t smallest_in_arr(int16_t arr[], uint8_t arr_size) { - int smallestVal = INT16_MAX; +uint8_t smallest_in_arr(uint8_t arr[], uint8_t arr_size) { + int smallestVal = UINT8_MAX; for (int i = 0; i < arr_size; i++) { if (arr[i] < smallestVal) { smallestVal = arr[i]; @@ -678,8 +668,6 @@ int16_t smallest_in_arr(int16_t arr[], uint8_t arr_size) { * Get difference between before and after in microseconds, accounting for * the fact the seconds place rolls over * - * @note timeval seems like the most portable way to get time, - * so it looks like i'm going with that */ inline int32_t get_elapsed_us(struct timeval before, struct timeval after) { int32_t elapsed_us; @@ -693,29 +681,6 @@ inline int32_t get_elapsed_us(struct timeval before, struct timeval after) { return elapsed_us; } -/** - * convert input array of int16_t to uint8_t -*/ -void int16_to_uint8_arr(int16_t *in_arr, uint8_t *out_arr, uint8_t arr_size) { - for (int i = 0; i < arr_size; i++) { - // out_arr[i] = (uint8_t) ((in_arr[i]) >> 8); - assert(in_arr[i] < 256 && in_arr[i] > -1 && "Can't cast OOB int16 to uint8!"); - out_arr[i] = (uint8_t) (in_arr[i]); - } -} - -/** - * convert input array of uint8_t to int16_t -*/ -void uint8_to_int16_arr(uint8_t *in_arr, int16_t *out_arr, uint8_t arr_size) { - for (int i = 0; i < arr_size; i++) { - // out_arr[i] = (uint8_t) ((in_arr[i]) >> 8); - out_arr[i] = (int16_t) (in_arr[i]); - - assert(out_arr[i] < 256 && out_arr[i] > -1 && "Converstion to int16 from uint8 OOB!"); - } -} - /** * a "Tetromino", or piece on the tetris board. * [piece_type][orientation][row,col offset location] diff --git a/tetris/tetris.h b/tetris/tetris.h index 14272b8..25fe27a 100644 --- a/tetris/tetris.h +++ b/tetris/tetris.h @@ -31,7 +31,8 @@ extern FILE *gamelog; #endif // how many rows and columns is the board? -// max allowed is 256,256 because anything larger is unreasonable +// max allowed is 128,128 since I want all locations to +// fit in a single byte #define TETRIS_ROWS 32 #define TETRIS_COLS 16 @@ -48,6 +49,11 @@ extern FILE *gamelog; // gravity_tick minimum where we won't decrease it any further past this point #define GRAVITY_TICK_RATE_FLOOR 20000 +// Points per line cleared, combos not implemented +// See: https://tetris.wiki/Scoring +static const uint16_t points_per_line_cleared[5] = {0, 100, 300, 500, 800}; + + // piece descriptions // S, Z, T, L, reverse L (J), square, long bar (I) enum piece_type {S_PIECE, Z_PIECE, T_PIECE, L_PIECE, J_PIECE, SQ_PIECE, I_PIECE}; @@ -58,17 +64,14 @@ enum piece_colors {S_CELL_COLOR, Z_CELL_COLOR, T_CELL_COLOR, L_CELL_COLOR, J_CEL // Define possible moves that can be taken by player enum player_move {T_NONE, T_UP, T_DOWN, T_LEFT, T_RIGHT, T_PLAYPAUSE, T_QUIT}; -// Points per line cleared, combos not implemented -// See: https://tetris.wiki/Scoring -static const uint16_t points_per_line_cleared[5] = {0, 100, 300, 500, 800}; /** * row,col location on tetris board, from top right. * Negative numbers allowed so this can be used for offsets */ typedef struct tetris_location { - int16_t row; - int16_t col; + int8_t row; + int8_t col; } tetris_location; // tetris piece descriptions - defined here to prevent multiple definition @@ -77,8 +80,9 @@ extern const tetris_location TETROMINOS[NUM_TETROMINOS][NUM_ORIENTATIONS][NUM_CE /** * Struct representing a single Tetromino * @param ptype - enum type of piece - * @param tetris_location loc - piece position - * @param orientation - piece orientation + * @param tetris_location loc - piece position [row,col] + * @param orientation - piece orientation [0-3] + * @param falling - bool, true if currently falling */ typedef struct TetrisPiece { enum piece_type ptype; @@ -89,11 +93,10 @@ typedef struct TetrisPiece { /** * Represents the game board - * @param board 2D int array representing board + * @param board 2D int8_t array representing board * -1 means unoccupied, >0 indicates cell color by piece_colors[] - * @param highest_occupied_row tallest point in current stack, tracked to - * avoid needless recomputation - * + * @param highest_occupied_row uint8_t tallest point in current stack, tracked to + * avoid needless recomputation and help indicate gameover condition */ typedef struct TetrisBoard { int8_t board[TETRIS_ROWS][TETRIS_COLS]; @@ -107,10 +110,11 @@ typedef struct TetrisBoard { * @param active_board 2D struct array representing entire board * (including falling piece) * @param game_over bool true if game over, false if not - * @param gravity_tick_rate usec between each gravity tick - * @param score player's current score - * @param level current level - * @param last_gravity_tick_usec last time active_piece was moved down + * @param gravity_tick_rate uint32_t usec between each gravity tick + * @param score uint32_t player's current score + * @param level uint32_t current level + * @param lines_cleared_since_last_level - uint8_t + * @param last_gravity_tick_usec - `struct timeval` last time active_piece was moved down */ typedef struct TetrisGame { TetrisBoard board; @@ -132,11 +136,13 @@ typedef struct TetrisGame { //////////////////////////////////////// // init/end functions + TetrisGame* create_game(void); void end_game(TetrisGame *tg); TetrisBoard init_board(void); -// THIS IS THE MAIN FUNCTION for using the library; all game state is handled internally +// This is the main function for using this library; all game state is handled internally + bool tg_tick(TetrisGame *tg, enum player_move move); @@ -146,6 +152,7 @@ bool check_and_spawn_new_piece(TetrisGame *tg); TetrisPiece create_rand_piece(TetrisGame *tg); // check game state conditions + bool check_valid_move(TetrisGame *tg, uint8_t player_move); bool test_piece_offset(TetrisBoard *tb, const tetris_location global_loc, const tetris_location move_offset); bool test_piece_rotate(TetrisBoard *tb, const TetrisPiece tp); @@ -158,9 +165,10 @@ void clear_rows(TetrisGame *tg, uint8_t top_row, uint8_t num_rows); bool check_game_over(TetrisGame *tg); // helper functions + bool val_in_arr(const uint8_t val, uint8_t arr[], const uint8_t arr_len); int32_t get_elapsed_us(struct timeval before, struct timeval after); -int16_t smallest_in_arr(int16_t arr[], const uint8_t arr_size); +uint8_t smallest_in_arr(uint8_t arr[], const uint8_t arr_size); void int16_to_uint8_arr(int16_t *in_arr, uint8_t *out_arr, uint8_t arr_size); void uint8_to_int16_arr(uint8_t *in_arr, int16_t *out_arr, uint8_t arr_size);