diff options
| author | 2019-10-28 15:49:51 -0400 | |
|---|---|---|
| committer | 2019-10-28 15:49:51 -0400 | |
| commit | d5ac165fdeb7b0c596a1d95845262b68f35e14d7 (patch) | |
| tree | eb05f1be1e52fd85f7a560ff4d485d73f42dbd6f /lib | |
| parent | 223b6c74f5534fa64b2b0f7d638619c5cb7a0342 (diff) | |
| download | cetris-d5ac165fdeb7b0c596a1d95845262b68f35e14d7.tar.gz cetris-d5ac165fdeb7b0c596a1d95845262b68f35e14d7.tar.bz2 cetris-d5ac165fdeb7b0c596a1d95845262b68f35e14d7.zip | |
move timers to cetris lib, add rulesets, add audio to sdl
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/cetris.c | 586 | ||||
| -rw-r--r-- | lib/cetris.h | 638 | ||||
| -rw-r--r-- | lib/meson.build | 11 | ||||
| -rw-r--r-- | lib/rules.c | 40 | ||||
| -rw-r--r-- | lib/rules.h | 7 | ||||
| -rw-r--r-- | lib/timer.c | 94 | ||||
| -rw-r--r-- | lib/timer.h | 4 |
7 files changed, 788 insertions, 592 deletions
diff --git a/lib/cetris.c b/lib/cetris.c new file mode 100644 index 0000000..288b82b --- /dev/null +++ b/lib/cetris.c @@ -0,0 +1,586 @@ +#include <assert.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "cetris.h" + +#ifdef BUILD_TESTS +#include "test.h" +#endif + +/* DEFAULT TETRIMINO */ + +CETRIS_EXPORT const piece_matrix default_matrices[7] = { + { 0b0000, 0b0110, 0b0110, 0b0000}, + { 0b0000, 0b1111, 0b0000, 0b0000}, + { 0b0000, 0b0110, 0b1100, 0b0000}, + { 0b0000, 0b1100, 0b0110, 0b0000}, + { 0b0000, 0b0010, 0b1110, 0b0000}, + { 0b0000, 0b1000, 0b1110, 0b0000}, + { 0b0000, 0b0100, 0b1110, 0b0000}}; + +/* SRS WALL KICK VALUES */ + +// https://tetris.wiki/SRS +static const vec2 srs_wall_kicks[8][5] = { + {{0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2}}, // 0->R + {{0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2}}, // R->0 + {{0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2}}, // R->2 + {{0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2}}, // 2->R + {{0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2}}, // 2->L + {{0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2}}, // L->2 + {{0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2}}, // L->0 + {{0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2}} // 0->L +}; + +static const vec2 srs_wall_kicks_i[8][5] = { + {{0, 0}, {-2, 0}, {1, 0}, {-2, -1}, {1, 2}}, // 0->R + {{0, 0}, {2, 0}, {-1, 0}, {2, 1}, {-1, -2}}, // R->0 + {{0, 0}, {-1, 0}, {2, 0}, {-1, 2}, {2, -1}}, // R->2 + {{0, 0}, {1, 0}, {-2, 0}, {1, -2}, {-2, 1}}, // 2->R + {{0, 0}, {2, 0}, {-1, 0}, {2, 1}, {-1, -2}}, // 2->L + {{0, 0}, {-2, 0}, {1, 0}, {-2, -1}, {1, 2}}, // L->2 + {{0, 0}, {1, 0}, {-2, 0}, {1, -2}, {-2, 1}}, // L->0 + {{0, 0}, {-1, 0}, {2, 0}, {-1, 2}, {2, -1}} // 0->L +}; + +static const vec2 basic_movements[5] = { + {0, 1}, // DOWN + {1, 0}, // RIGHT + {-1, 0} // LEFT +}; + +static void update_board(cetris_game *g); + +static void set_piece(cetris_game *g, uint8_t type, tetrimino* mino) { + memset(mino, 0, sizeof(tetrimino)); + + mino->t = type; + memcpy(mino->m, default_matrices[type], sizeof(piece_matrix)); + + /* Pieces should spawn so that on the first down + * tick the bottom row will show. Values here are adjusted + * for the default 4x4 matricies for each piece */ + mino->pos.x = g->config.mino_start_x; + mino->pos.y = g->config.mino_start_y; + if (type == MINO_I) mino->pos.y++; +} + +static void shuffle_queue(cetris_game *g) { + for (int i = 0; i < 7; i++) { + uint8_t rand_index = rand() % 7; + uint8_t tmp = g->next_queue[i]; + g->next_queue[i] = g->next_queue[rand_index]; + g->next_queue[rand_index] = tmp; + } +} + +static int check_matrix(cetris_game *g, piece_matrix *m) { + for (uint8_t y = 0; y < 4; y++) { + for (uint8_t x = 0; x < 4; x++) { + vec2 r = (vec2){x + g->current.pos.x, y + g->current.pos.y}; + if (r.y < 0) + continue; + if (((*m)[y]>>(3 - x))&1) { + if (r.x >= g->config.board_x || r.x < 0) + return 0; + if (r.y >= g->config.board_y) + return -1; + if (g->board[r.x][r.y] & SLOT_OCCUPIED) + return -1; + } + } + } + return 1; +} + +// TODO: hard score +static void add_score(cetris_game *g, int lines) { + if (!g->tspin && !g->mini_tspin) { + switch (lines) { + case 1: + g->score += 100 * g->level; + break; + case 2: + g->score += 300 * g->level; + break; + case 3: + g->score += 500 * g->level; + break; + case 4: + g->score += 800 * g->level; + break; + } + } else if (g->tspin) { + switch (lines) { + case 0: + g->score += 400 * g->level; + break; + case 1: + g->score += 800 * g->level; + break; + case 2: + g->score += 1200 * g->level; + break; + case 3: + g->score += 1600 * g->level; + break; + } + g->tspin = false; + } else if (g->mini_tspin) { + switch (lines) { + case 0: + g->score += 100 * g->level; + break; + case 1: + g->score += 200 * g->level; + break; + case 2: + g->score += 400 * g->level; + break; + } + g->mini_tspin = false; + } +} + +static void make_ghosts(cetris_game *g) { + int8_t orig_y = g->current.pos.y; + while (check_matrix(g, &g->current.m) > 0) { + g->current.pos.y++; + } + if (g->current.pos.y == orig_y) { + g->current.ghost_y = orig_y; + } else { + g->current.ghost_y = g->current.pos.y - 1; + } + g->current.pos.y = orig_y; +} + +static void move_current(cetris_game *g, uint8_t move) { + if (g->game_over || g->next_piece_tick) + return; + + g->current.pos.y += basic_movements[move].y; + g->current.pos.x += basic_movements[move].x; + + int check = check_matrix(g, &g->current.m); + if (check <= 0) { + g->current.pos.y -= basic_movements[move].y; + g->current.pos.x -= basic_movements[move].x; + + if (move == DOWN && check == -1) { + if (!g->current.force_lock_tick && g->config.force_lock) + g->current.force_lock_tick = g->tick + g->config.force_lock; + if (!g->current.lock_tick) + g->current.lock_tick = g->tick + g->config.lock_delay; + } + } else { + if (g->current.lock_tick) + g->current.lock_tick = g->tick + g->config.lock_delay; + if (move == DOWN && g->held_moves[DOWN]) + g->score++; + } + + update_board(g); +} + +static void next_piece(cetris_game *g) { + g->next_drop_tick = 0; + g->next_piece_tick = 0; + + set_piece(g, g->piece_queue[g->current_index], &g->current); + + if (check_matrix(g, &g->current.m) <= 0) { + g->game_over = true; + } + + if (!g->game_over) { + move_current(g, DOWN); + } + + g->current_index++; + if (g->current_index == 7) { + memcpy(&g->piece_queue, &g->next_queue, sizeof(g->piece_queue)); + g->current_index = 0; + shuffle_queue(g); + } + + update_board(g); +} + +static void lock_current(cetris_game *g) { + g->current.locked = true; + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + if ((g->current.m[y]>>(3 - x))&1) { + g->board[g->current.pos.x + x][g->current.pos.y + y] |= SLOT_OCCUPIED; + g->board[g->current.pos.x + x][g->current.pos.y + y] |= g->current.t << 5; + } + } + } + + if (g->current.pos.y < g->highest_line) { + g->highest_line = g->current.pos.y; + } + + update_board(g); +} + +static void hard_drop(cetris_game *g) { + if (g->game_over || g->next_piece_tick) + return; + + int drop_count = 0; + while (check_matrix(g, &g->current.m) > 0) { + g->current.pos.y++; + drop_count++; + } + g->current.pos.y--; + drop_count--; + + g->score += 2 * drop_count; // 2 score for each hard-drop'd cell + + lock_current(g); + update_board(g); +} + +static void rotate_matrix(cetris_game *g, piece_matrix *m, bool clockwise) { + for (uint8_t x = 0; x < 4; x++) { + for (uint8_t y = 0; y < 4; y++) { + if ((g->current.m[y]>>(3 - x))&1) { + uint8_t new_x = (clockwise) ? 1 - (y - 2) : 1 + (y - 2); + uint8_t new_y = (clockwise) ? 2 + (x - 1) : 2 - (x - 1); + + if (g->current.t == MINO_I) { + clockwise ? new_y-- : new_x++; + } + + (*m)[new_y] |= (uint8_t)0b1000 >> (new_x); + } + } + } +} + +static void rotate_piece(cetris_game *g, bool clockwise) { + if (g->game_over || g->next_piece_tick) + return; + if (g->current.t == MINO_O) + return; + + uint8_t next = 0; + uint8_t wall_kick = 0; + if (clockwise) { + next = (g->current.r + 1)%4; + wall_kick = g->current.r * 2; + } else { + next = ((g->current.r - 1) + 4)%4; + wall_kick = (next * 2) + 1; + } + + piece_matrix m; + memset(&m, 0, sizeof(piece_matrix)); + + rotate_matrix(g, &m, clockwise); + + vec2 kick; + bool set_current = false; + bool did_kick = false; + for (int i = 0; i < 4; i++) { + if (g->current.t == MINO_I) { + kick = srs_wall_kicks_i[wall_kick][i]; + } else { + kick = srs_wall_kicks[wall_kick][i]; + } + + g->current.pos.x += kick.x; + g->current.pos.y -= kick.y; + + if (check_matrix(g, &m) > 0) { + set_current = true; + if (i > 0) did_kick = true; + break; + } + + g->current.pos.x -= kick.x; + g->current.pos.y += kick.y; + } + + if (set_current) { + /* check for tspin */ + if (g->current.t == MINO_T) { + bool did_tspin = true; + for (int i = 1; i < 5; i++) { + g->current.pos.x += basic_movements[i].x; + g->current.pos.y += basic_movements[i].y; + + if (check_matrix(g, &m) == 1) + did_tspin = false; + + g->current.pos.x -= basic_movements[i].x; + g->current.pos.y -= basic_movements[i].y; + } + + if (did_tspin) { + if (did_kick) g->mini_tspin = true; + else g->tspin = true; + } + } + + g->current.r = next; + memcpy(g->current.m, &m, sizeof(piece_matrix)); + update_board(g); + } +} + +void update_board(cetris_game *g) { + if (g->game_over) + return; + + int lines_cleared = 0; + for (int y = g->highest_line; y < g->config.board_y; y++) { + bool clear_line = true; + for (int x = 0; x < g->config.board_x; x++) { + if (!(g->board[x][y] & SLOT_OCCUPIED) + || g->line_remove_tick[y] > 0) { + clear_line = false; + } + } + + if (g->config.wait_on_clear) { + // remove tick only tracked on first block of line + if (g->line_remove_tick[y] && g->line_remove_tick[y] <= g->tick) { + g->line_remove_tick[y] = 0; + for (int s = y - 1; s >= 0; s--) { + for (int x = 0; x < g->config.board_x; x++) { + g->board[x][s + 1] = g->board[x][s]; + } + } + } + if (clear_line) { + g->line_remove_tick[y] = g->tick + g->config.line_delay_clear; + lines_cleared++; + } + } else if (clear_line) { + for (int s = y - 1; s >= 0; s--) { + for (int x = 0; x < g->config.board_x; x++) { + g->board[x][s + 1] = g->board[x][s]; + } + } + lines_cleared++; + } + } + + make_ghosts(g); + + if (g->current.locked && !g->next_piece_tick) { + if (lines_cleared > 0) { + g->next_piece_tick = g->tick + g->config.next_piece_delay; + } else { + next_piece(g); + g->line_combo = 0; + } + } + + if (g->tspin || g->mini_tspin) { + add_score(g, lines_cleared); + } else if (lines_cleared > 0) { + add_score(g, lines_cleared); + } + + g->lines += lines_cleared; + if (lines_cleared > 0) { + g->line_combo++; + g->line_event = true; + } + if (g->lines >= (g->level * 10)) { + g->level++; + } +} + +CETRIS_EXPORT void hold_piece(cetris_game *g) { + if (g->current.held) return; + if (g->piece_held) { + tetrimino tmp = g->current; + g->current = g->held; + g->held = tmp; + } else { + set_piece(g, g->current.t, &g->held); + g->piece_held = true; + next_piece(g); + } + g->current.held = true; + update_board(g); +} + +#if CETRIS_ENABLE_DAS +CETRIS_EXPORT void unhold_move(cetris_game* g, uint8_t move) { + if (g->das_move == move) { + if (move == LEFT && g->held_moves[RIGHT]) { + g->das_move = RIGHT; + g->das_wait = g->tick + g->config.das_das; + } else if (move == RIGHT && g->held_moves[LEFT]) { + g->das_move = LEFT; + g->das_wait = g->tick + g->config.das_das; + } else { + g->das_wait = 0; + } + g->next_das_move = 0; + } + if (move == DOWN) g->next_drop_tick = 0; + g->held_moves[move] = 0; +} +#endif + +CETRIS_EXPORT void move_piece(cetris_game *g, uint8_t move) { +#if CETRIS_ENABLE_DAS + if (g->held_moves[move]) return; + if (move == LEFT || move == RIGHT) { + if ((move != g->das_move) || !g->das_wait) { + g->das_move = move; + if (!g->waiting) { + g->das_wait = g->tick + g->config.das_das; + g->next_das_move = 0; + } + else g->next_das_move = 1; + } + } + if (move == DOWN) g->next_drop_tick = g->tick + g->config.drop_period; + g->held_moves[move] = 1; +#endif + + if (g->waiting) return; + + switch (move) { + case LEFT: + case RIGHT: + case DOWN: + move_current(g, move); + break; + case HARD_DROP: + hard_drop(g); + break; + case ROTATE_CW: + rotate_piece(g, 1); + break; + case ROTATE_CCW: + rotate_piece(g, 0); + break; + } + +} + +CETRIS_EXPORT void init_game(cetris_game *g, cetris_config* c) { + srand(time(NULL)); + +#ifdef BUILD_TESTS + //apply_test_board(g, TSPIN_NO_LINES); +#endif + + cetris_config config; + if (!c) { + config = g->config; + } else { + config = *c; + } + + // check for config errorsa + if (config.next_piece_delay < config.line_delay_clear) { + config.next_piece_delay = config.line_delay_clear; + } + + if (!config.wait_on_clear) { + config.next_piece_delay = 0; + } + + memset(g, 0, sizeof(cetris_game)); + + memcpy(&g->config, &config, sizeof(cetris_config)); + + g->board = (uint8_t **)malloc(sizeof(uint8_t *) * config.board_x); + for (int i = 0; i < config.board_x; i++) { + g->board[i] = (uint8_t *)malloc(sizeof(uint8_t) * config.board_y); + memset(g->board[i], 0, sizeof(uint8_t) * config.board_y); + } + + g->line_remove_tick = (ctick *)malloc(sizeof(ctick) * config.board_y); + memset(g->line_remove_tick, 0, sizeof(ctick) * config.board_y); + + g->level = config.starting_level; + g->waiting = true; + + g->highest_line = config.board_y; + + for (int i = 0; i < 7; i++) { + g->next_queue[i] = i; + } + + shuffle_queue(g); + memcpy(&g->piece_queue, &g->next_queue, sizeof(g->piece_queue)); + shuffle_queue(g); + + next_piece(g); +} + +CETRIS_EXPORT bool update_game_tick(cetris_game *g) { + if (g->game_over) + return false; + + if (g->next_piece_tick && g->tick >= g->next_piece_tick) { + next_piece(g); + } + + if (g->next_piece_tick) + return true; + + bool did_move = false; + if (g->next_drop_tick && g->tick >= g->next_drop_tick) { + move_current(g, DOWN); + g->next_drop_tick = 0; + did_move = true; + } + + if (!g->next_drop_tick) { + if (g->held_moves[DOWN]) { + g->next_drop_tick = g->tick + g->config.drop_period; + } else { + if (g->level <= 20) { + g->next_drop_tick = g->tick + g->config.levels[g->level - 1]; + } else { + g->next_drop_tick = g->tick + g->config.levels[19]; + } + } + } + + /* lock piece if it was hovering for CETRIS_LOCK_DELAY */ + if (!g->next_piece_tick && ((g->current.lock_tick && g->current.lock_tick <= g->tick) + || (g->current.force_lock_tick && g->current.force_lock_tick <= g->tick))) { + g->current.pos.y++; + int8_t res = check_matrix(g, &g->current.m); + g->current.pos.y--; + if (res <= 0) { + lock_current(g); + did_move = true; + } + g->current.lock_tick = 0; + } + +#if CETRIS_ENABLE_DAS + if ((g->next_das_move && g->tick >= g->next_das_move) || g->next_das_move == 1) { + if (!g->waiting) move_current(g, g->das_move); + g->next_das_move = g->tick + g->config.das_arr; + } else if (!g->next_das_move && g->das_wait && g->tick >= g->das_wait) { + g->next_das_move = g->tick + g->config.das_arr; + } +#endif + + if (did_move) update_board(g); + + if (g->config.win_condition(g)) g->game_over = true; + + return true; +} + + diff --git a/lib/cetris.h b/lib/cetris.h index 59cdf27..3a71999 100644 --- a/lib/cetris.h +++ b/lib/cetris.h @@ -1,22 +1,14 @@ #ifndef CETRIS_H #define CETRIS_H -#include <assert.h> -#include <stdbool.h> -#include <stdint.h> -#include <stdlib.h> -#include <string.h> -#include <time.h> - -#ifdef BUILD_TESTS -#include "test.h" +#ifdef __linux__ +#define _GNU_SOURCE #endif -#define CETRIS_EXPORT +#include <stdint.h> +#include <stdbool.h> -#define CETRIS_BOARD_X 10 -#define CETRIS_BOARD_Y 40 -#define CETRIS_BOARD_VISABLE 20 +#define CETRIS_EXPORT #define CETRIS_HI_RES 1 @@ -30,14 +22,6 @@ typedef uint16_t ctick; #define CETRIS_ENABLE_DAS 1 -#define CETRIS_DROP_PERIOD 2 -#define CETRIS_NEXT_PIECE_DELAY 40 -#define CETRIS_LINE_CLEAR_DELAY 40 -#define CETRIS_LOCK_DELAY 30 -#define CETRIS_WAIT_ON_CLEAR 0 - -#define CETRIS_STARTING_LEVEL 4 - typedef struct { int8_t x; int8_t y; @@ -66,6 +50,15 @@ enum { ONCE_LEFT }; +enum { + DOWN, + RIGHT, + LEFT, + ROTATE_CCW, + ROTATE_CW, + HARD_DROP +}; + typedef struct { vec2 pos; uint8_t t; @@ -78,33 +71,38 @@ typedef struct { piece_matrix m; } tetrimino; -typedef enum { - DOWN, - RIGHT, - LEFT, - ROTATE_CCW, - ROTATE_CW, - HARD_DROP -} input_t; +typedef struct cetris_game cetris_game; typedef struct { -#if CETRIS_ENABLE_DAS - ctick das_arr; - ctick das_das; -#endif - ctick drop_period; ctick next_piece_delay; ctick line_delay_clear; ctick lock_delay; ctick force_lock; + +#if CETRIS_ENABLE_DAS + ctick das_arr; + ctick das_das; +#endif + uint8_t board_x; + uint8_t board_y; + uint8_t board_visible; + + uint8_t mino_start_x; + uint8_t mino_start_y; + + uint8_t starting_level; bool wait_on_clear; + + ctick *levels; + + bool (*win_condition)(cetris_game *); } cetris_config; -typedef struct { +struct cetris_game { // playfield represented by a 2d array - uint8_t board[CETRIS_BOARD_X][CETRIS_BOARD_Y]; + uint8_t **board; int8_t highest_line; // queue of all 7 possible tetrimino @@ -125,7 +123,12 @@ typedef struct { ctick next_drop_tick; ctick next_piece_tick; ctick down_move_tick; - ctick line_remove_tick[CETRIS_BOARD_Y]; + ctick *line_remove_tick; + +#if CETRIS_HI_RES + // microsecond accuracy timer + long long timer; +#endif #if CETRIS_ENABLE_DAS ctick das_wait; @@ -135,8 +138,10 @@ typedef struct { #endif // progress trackers - uint8_t lines; uint8_t level; + uint8_t lines; + uint8_t line_combo; + bool line_event; bool game_over; // scoring flags @@ -148,564 +153,13 @@ typedef struct { // config cetris_config config; -} cetris_game; - -const piece_matrix default_matrices[7] = { - { 0b0000, 0b0110, 0b0110, 0b0000}, - { 0b0000, 0b1111, 0b0000, 0b0000}, - { 0b0000, 0b0110, 0b1100, 0b0000}, - { 0b0000, 0b1100, 0b0110, 0b0000}, - { 0b0000, 0b0010, 0b1110, 0b0000}, - { 0b0000, 0b1000, 0b1110, 0b0000}, - { 0b0000, 0b0100, 0b1110, 0b0000}}; - -/* SRS WALL KICK VALUES */ - -// https://tetris.wiki/SRS -static const vec2 srs_wall_kicks[8][5] = { - {{0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2}}, // 0->R - {{0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2}}, // R->0 - {{0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2}}, // R->2 - {{0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2}}, // 2->R - {{0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2}}, // 2->L - {{0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2}}, // L->2 - {{0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2}}, // L->0 - {{0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2}} // 0->L -}; - -static const vec2 srs_wall_kicks_i[8][5] = { - {{0, 0}, {-2, 0}, {1, 0}, {-2, -1}, {1, 2}}, // 0->R - {{0, 0}, {2, 0}, {-1, 0}, {2, 1}, {-1, -2}}, // R->0 - {{0, 0}, {-1, 0}, {2, 0}, {-1, 2}, {2, -1}}, // R->2 - {{0, 0}, {1, 0}, {-2, 0}, {1, -2}, {-2, 1}}, // 2->R - {{0, 0}, {2, 0}, {-1, 0}, {2, 1}, {-1, -2}}, // 2->L - {{0, 0}, {-2, 0}, {1, 0}, {-2, -1}, {1, 2}}, // L->2 - {{0, 0}, {1, 0}, {-2, 0}, {1, -2}, {-2, 1}}, // L->0 - {{0, 0}, {-1, 0}, {2, 0}, {-1, 2}, {2, -1}} // 0->L }; -static const vec2 basic_movements[5] = { - {0, 1}, // DOWN - {1, 0}, // RIGHT - {-1, 0} // LEFT -}; - -// https://tetris.fandom.com/wiki/Tetris_Worlds -#if CETRIS_HI_RES -static const int level_drop_delay[20] = {1000, 793, 618, 473, 355, 262, 189, 134, 94, 64, - 43, 28, 18, 11, 7, 4, 2, 1, 1, 1}; -#else -static const int level_drop_delay[20] = {60, 48, 37, 28, 21, 16, 11, 8, 6, 4, - 3, 2, 1, 1, 1, 1, 1, 1, 1, 1}; -#endif - -static void update_board(cetris_game *g); - CETRIS_EXPORT bool update_game_tick(cetris_game *g); -CETRIS_EXPORT void hold_piece(cetris_game *g); +CETRIS_EXPORT void move_piece(cetris_game *g, uint8_t move); +CETRIS_EXPORT void unhold_move(cetris_game* g, uint8_t move); CETRIS_EXPORT void init_game(cetris_game *g, cetris_config *c); CETRIS_EXPORT void hold_piece(cetris_game *g); - -static void set_piece(uint8_t type, tetrimino* mino) { - memset(mino, 0, sizeof(tetrimino)); - - mino->t = type; - memcpy(mino->m, default_matrices[type], sizeof(piece_matrix)); - - /* Pieces should spawn so that on the first down - * tick the bottom row will show. Values here are adjusted - * for the default 4x4 matricies for each piece */ - mino->pos.x = 3; - mino->pos.y = (type == MINO_I) ? 17 : 16; -} - -static void shuffle_queue(cetris_game *g) { - for (int i = 0; i < 7; i++) { - uint8_t rand_index = rand() % 7; - uint8_t tmp = g->next_queue[i]; - g->next_queue[i] = g->next_queue[rand_index]; - g->next_queue[rand_index] = tmp; - } -} - -static int check_matrix(cetris_game *g, piece_matrix *m) { - for (uint8_t y = 0; y < 4; y++) { - for (uint8_t x = 0; x < 4; x++) { - vec2 r = (vec2){x + g->current.pos.x, y + g->current.pos.y}; - if (r.y < 0) - continue; - if (((*m)[y]>>(3 - x))&1) { - if (r.x >= CETRIS_BOARD_X || r.x < 0) - return 0; - if (r.y >= CETRIS_BOARD_Y) - return -1; - if (g->board[r.x][r.y] & SLOT_OCCUPIED) - return -1; - } - } - } - return 1; -} - -// TODO: hard score -static void add_score(cetris_game *g, int lines) { - if (!g->tspin && !g->mini_tspin) { - switch (lines) { - case 1: - g->score += 100 * g->level; - break; - case 2: - g->score += 300 * g->level; - break; - case 3: - g->score += 500 * g->level; - break; - case 4: - g->score += 800 * g->level; - break; - } - } else if (g->tspin) { - switch (lines) { - case 0: - g->score += 400 * g->level; - break; - case 1: - g->score += 800 * g->level; - break; - case 2: - g->score += 1200 * g->level; - break; - case 3: - g->score += 1600 * g->level; - break; - } - g->tspin = false; - } else if (g->mini_tspin) { - switch (lines) { - case 0: - g->score += 100 * g->level; - break; - case 1: - g->score += 200 * g->level; - break; - case 2: - g->score += 400 * g->level; - break; - } - g->mini_tspin = false; - } -} - -static void make_ghosts(cetris_game *g) { - int8_t orig_y = g->current.pos.y; - while (check_matrix(g, &g->current.m) > 0) { - g->current.pos.y++; - } - if (g->current.pos.y == orig_y) { - g->current.ghost_y = orig_y; - } else { - g->current.ghost_y = g->current.pos.y - 1; - } - g->current.pos.y = orig_y; -} - -static void move_current(cetris_game *g, uint8_t move) { - if (g->game_over || g->next_piece_tick) - return; - - g->current.pos.y += basic_movements[move].y; - g->current.pos.x += basic_movements[move].x; - - int check = check_matrix(g, &g->current.m); - if (check <= 0) { - g->current.pos.y -= basic_movements[move].y; - g->current.pos.x -= basic_movements[move].x; - - if (move == DOWN && check == -1) { - if (!g->current.force_lock_tick) - g->current.force_lock_tick = g->tick + g->config.force_lock; - if (!g->current.lock_tick) - g->current.lock_tick = g->tick + g->config.lock_delay; - } - } else { - if (g->current.lock_tick) - g->current.lock_tick = g->tick + g->config.lock_delay; - if (move == DOWN && g->held_moves[DOWN]) - g->score++; - } - - update_board(g); -} - -static void next_piece(cetris_game *g) { - g->next_drop_tick = 0; - g->next_piece_tick = 0; - - set_piece(g->piece_queue[g->current_index], &g->current); - - if (check_matrix(g, &g->current.m) <= 0) { - g->game_over = true; - } - - if (!g->game_over) { - move_current(g, DOWN); - } - - g->current_index++; - if (g->current_index == 7) { - memcpy(&g->piece_queue, &g->next_queue, sizeof(g->piece_queue)); - g->current_index = 0; - shuffle_queue(g); - } - - update_board(g); -} - -static void lock_current(cetris_game *g) { - g->current.locked = true; - for (int y = 0; y < 4; y++) { - for (int x = 0; x < 4; x++) { - if ((g->current.m[y]>>(3 - x))&1) { - g->board[g->current.pos.x + x][g->current.pos.y + y] |= SLOT_OCCUPIED; - g->board[g->current.pos.x + x][g->current.pos.y + y] |= g->current.t << 5; - } - } - } - - if (g->current.pos.y < g->highest_line) { - g->highest_line = g->current.pos.y; - } - - update_board(g); -} - -static void hard_drop(cetris_game *g) { - if (g->game_over || g->next_piece_tick) - return; - - int drop_count = 0; - while (check_matrix(g, &g->current.m) > 0) { - g->current.pos.y++; - drop_count++; - } - g->current.pos.y--; - drop_count--; - - g->score += 2 * drop_count; // 2 score for each hard-drop'd cell - - lock_current(g); - update_board(g); -} - -static void rotate_matrix(cetris_game *g, piece_matrix *m, bool clockwise) { - for (uint8_t x = 0; x < 4; x++) { - for (uint8_t y = 0; y < 4; y++) { - if ((g->current.m[y]>>(3 - x))&1) { - uint8_t new_x = (clockwise) ? 1 - (y - 2) : 1 + (y - 2); - uint8_t new_y = (clockwise) ? 2 + (x - 1) : 2 - (x - 1); - - if (g->current.t == MINO_I) { - clockwise ? new_y-- : new_x++; - } - - (*m)[new_y] |= (uint8_t)0b1000 >> (new_x); - } - } - } -} - -static void rotate_piece(cetris_game *g, bool clockwise) { - if (g->game_over || g->next_piece_tick) - return; - if (g->current.t == MINO_O) - return; - - uint8_t next = 0; - uint8_t wall_kick = 0; - if (clockwise) { - next = (g->current.r + 1)%4; - wall_kick = g->current.r * 2; - } else { - next = ((g->current.r - 1) + 4)%4; - wall_kick = (next * 2) + 1; - } - - piece_matrix m; - memset(&m, 0, sizeof(piece_matrix)); - - rotate_matrix(g, &m, clockwise); - - vec2 kick; - bool set_current = false; - bool did_kick = false; - for (int i = 0; i < 4; i++) { - if (g->current.t == MINO_I) { - kick = srs_wall_kicks_i[wall_kick][i]; - } else { - kick = srs_wall_kicks[wall_kick][i]; - } - - g->current.pos.x += kick.x; - g->current.pos.y -= kick.y; - - if (check_matrix(g, &m) > 0) { - set_current = true; - if (i > 0) did_kick = true; - break; - } - - g->current.pos.x -= kick.x; - g->current.pos.y += kick.y; - } - - if (set_current) { - /* check for tspin */ - if (g->current.t == MINO_T) { - bool did_tspin = true; - for (int i = 1; i < 5; i++) { - g->current.pos.x += basic_movements[i].x; - g->current.pos.y += basic_movements[i].y; - - if (check_matrix(g, &m) == 1) - did_tspin = false; - - g->current.pos.x -= basic_movements[i].x; - g->current.pos.y -= basic_movements[i].y; - } - - if (did_tspin) { - if (did_kick) g->mini_tspin = true; - else g->tspin = true; - } - } - - g->current.r = next; - memcpy(g->current.m, &m, sizeof(piece_matrix)); - update_board(g); - } -} - -void update_board(cetris_game *g) { - if (g->game_over) - return; - - int lines_cleared = 0; - for (int y = g->highest_line; y < CETRIS_BOARD_Y; y++) { - bool clear_line = true; - for (int x = 0; x < CETRIS_BOARD_X; x++) { - if (!(g->board[x][y] & SLOT_OCCUPIED) - || g->line_remove_tick[y] > 0) { - clear_line = false; - } - } - - if (g->config.wait_on_clear) { - // remove tick only tracked on first block of line - if (g->line_remove_tick[y] && g->line_remove_tick[y] <= g->tick) { - g->line_remove_tick[y] = 0; - for (int s = y - 1; s >= 0; s--) { - for (int x = 0; x < CETRIS_BOARD_X; x++) { - g->board[x][s + 1] = g->board[x][s]; - } - } - } - if (clear_line) { - g->line_remove_tick[y] = g->tick + g->config.line_delay_clear; - lines_cleared++; - } - } else if (clear_line) { - for (int s = y - 1; s >= 0; s--) { - for (int x = 0; x < CETRIS_BOARD_X; x++) { - g->board[x][s + 1] = g->board[x][s]; - } - } - lines_cleared++; - } - } - - make_ghosts(g); - - assert(lines_cleared <= 4); - - if (g->current.locked && !g->next_piece_tick) { - if (lines_cleared > 0) { - g->next_piece_tick = g->tick + g->config.next_piece_delay; - } else { - next_piece(g); - } - } - - if (lines_cleared > 0 || g->tspin || g->mini_tspin) { - add_score(g, lines_cleared); - if (lines_cleared > 0) { - g->lines += lines_cleared; - if (g->lines >= 20) g->game_over = true; - if (g->lines >= (g->level * 10)) - g->level++; - } - } -} - -CETRIS_EXPORT void hold_piece(cetris_game *g) { - if (g->current.held) return; - if (g->piece_held) { - tetrimino tmp = g->current; - g->current = g->held; - g->held = tmp; - } else { - set_piece(g->current.t, &g->held); - g->piece_held = true; - next_piece(g); - } - g->current.held = true; - update_board(g); -} - -#if CETRIS_ENABLE_DAS -CETRIS_EXPORT void unhold_piece(cetris_game* g, input_t move) { - if (g->das_move == move) { - if (move == LEFT && g->held_moves[RIGHT]) { - g->das_move = RIGHT; - g->das_wait = g->tick + g->config.das_das; - } else if (move == RIGHT && g->held_moves[LEFT]) { - g->das_move = LEFT; - g->das_wait = g->tick + g->config.das_das; - } else { - g->das_wait = 0; - } - g->next_das_move = 0; - } - g->held_moves[move] = 0; -} -#endif - -CETRIS_EXPORT void move_piece(cetris_game *g, uint8_t move) { -#if CETRIS_ENABLE_DAS - if (g->held_moves[move]) return; - if (move == LEFT || move == RIGHT) { - if ((move != g->das_move) || !g->das_wait) { - g->das_move = move; - if (!g->waiting) { - g->das_wait = g->tick + g->config.das_das; - g->next_das_move = 0; - } - else g->next_das_move = 1; - } - } - if (move == DOWN) g->next_drop_tick = g->tick + g->config.drop_period; - g->held_moves[move] = 1; -#endif - - if (g->waiting) return; - - switch (move) { - case LEFT: - case RIGHT: - case DOWN: - move_current(g, move); - break; - case HARD_DROP: - hard_drop(g); - break; - case ROTATE_CW: - rotate_piece(g, 1); - break; - case ROTATE_CCW: - rotate_piece(g, 0); - break; - } - -} - -CETRIS_EXPORT void init_game(cetris_game *g, cetris_config* c) { - - /* check for config errors */ - assert(CETRIS_NEXT_PIECE_DELAY >= CETRIS_LINE_CLEAR_DELAY); - - srand(time(NULL)); - -#ifdef BUILD_TESTS - //apply_test_board(g, TSPIN_NO_LINES); -#endif - - memset(g, 0, sizeof(cetris_game)); - memcpy(&g->config, c, sizeof(cetris_config)); - - g->level = CETRIS_STARTING_LEVEL; - g->highest_line = CETRIS_BOARD_Y; - - for (int i = 0; i < 7; i++) { - g->next_queue[i] = i; - } - - shuffle_queue(g); - memcpy(&g->piece_queue, &g->next_queue, sizeof(g->piece_queue)); - shuffle_queue(g); - - next_piece(g); -} - -CETRIS_EXPORT bool update_game_tick(cetris_game *g) { - if (g->game_over) - return false; - -#if CETRIS_HI_RES - //g->tick += .01f; -#else - g->tick++; -#endif - - if (g->next_piece_tick && g->tick >= g->next_piece_tick) { - next_piece(g); - } - - if (g->next_piece_tick) - return true; - - bool did_move = false; - if (g->next_drop_tick && g->tick >= g->next_drop_tick) { - move_current(g, DOWN); - g->next_drop_tick = 0; - did_move = true; - } - - if (!g->next_drop_tick) { - if (g->held_moves[DOWN]) { - g->next_drop_tick = g->tick + g->config.drop_period; - } else { - if (g->level <= 20) { - g->next_drop_tick = g->tick + level_drop_delay[g->level - 1]; - } else { - g->next_drop_tick = g->tick + level_drop_delay[19]; - } - } - } - - /* lock piece if it was hovering for CETRIS_LOCK_DELAY */ - if (!g->next_piece_tick && ((g->current.lock_tick && g->current.lock_tick <= g->tick) - || (g->current.force_lock_tick && g->current.force_lock_tick <= g->tick))) { - g->current.pos.y++; - int8_t res = check_matrix(g, &g->current.m); - g->current.pos.y--; - if (res <= 0) { - lock_current(g); - did_move = true; - } - g->current.lock_tick = 0; - } - -#if CETRIS_ENABLE_DAS - if ((g->next_das_move && g->tick >= g->next_das_move) || g->next_das_move == 1) { - if (!g->waiting) move_current(g, g->das_move); - g->next_das_move = g->tick + g->config.das_arr; - } else if (!g->next_das_move && g->das_wait && g->tick >= g->das_wait) { - g->next_das_move = g->tick + g->config.das_arr; - } -#endif - - if (did_move) update_board(g); - - return true; -} +CETRIS_EXPORT const piece_matrix default_matrices[7]; #endif /* CETRIS_H */ diff --git a/lib/meson.build b/lib/meson.build new file mode 100644 index 0000000..b37c340 --- /dev/null +++ b/lib/meson.build @@ -0,0 +1,11 @@ +src = ['cetris.c', 'timer.c', 'rules.c'] + +threads = dependency('threads') + +cetris_lib = static_library('cetris', src, + dependencies: threads, + include_directories: cetris_inc, + install: false) + +cetris = declare_dependency(include_directories: cetris_inc, + link_with: cetris_lib) diff --git a/lib/rules.c b/lib/rules.c new file mode 100644 index 0000000..ce06d83 --- /dev/null +++ b/lib/rules.c @@ -0,0 +1,40 @@ +#include <cetris.h> +#include <rules.h> + +bool twenty_line_sprint(cetris_game *g) { + if (g->lines >= 20) return true; + return false; +} + +bool forty_line_sprint(cetris_game *g) { + if (g->lines >= 40) return true; + return false; +} + +bool marathon(cetris_game *g) { + return false; +} + +cetris_config tetris_ds_config = { + .board_x = 10, + .board_y = 43, + .board_visible = 20, + .mino_start_x = 3, + .mino_start_y = 19, + .drop_period = 83, + .next_piece_delay = 666, + .line_delay_clear = 666, + .lock_delay = 500, + .force_lock = 0, + .das_arr = 83, + .das_das = 183, + .starting_level = 1, + .wait_on_clear = true, + .win_condition = marathon +}; + +// https://tetris.fandom.com/wiki/Tetris_Worlds +ctick tetris_worlds_levels[20] = {1000, 793, 618, 473, 355, 262, 189, 134, 94, 64, + 43, 28, 18, 11, 7, 4, 2, 1, 1, 1}; + + diff --git a/lib/rules.h b/lib/rules.h new file mode 100644 index 0000000..190e4b7 --- /dev/null +++ b/lib/rules.h @@ -0,0 +1,7 @@ +#include <cetris.h> + +cetris_config tetris_ds_config; +ctick tetris_worlds_levels[20]; + +bool twenty_line_sprint(cetris_game *g); +bool forty_line_sprint(cetris_game *g); diff --git a/lib/timer.c b/lib/timer.c new file mode 100644 index 0000000..0a1504c --- /dev/null +++ b/lib/timer.c @@ -0,0 +1,94 @@ +#include <cetris.h> + +#ifdef _WIN32 +#include <window.h> + +#if CETRIS_HI_RES +DWORD WINAPI cetris_game_loop(void* data) { + cetris_game* game = (cetris_game*)data; + LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds; + LARGE_INTEGER Frequency; + QueryPerformanceFrequency(&Frequency); + QueryPerformanceCounter(&StartingTime); + while(1) { + if (game->waiting) break; + QueryPerformanceCounter(&EndingTime); + ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart; + ElapsedMicroseconds.QuadPart *= 1000000; + ElapsedMicroseconds.QuadPart /= Frequency.QuadPart; + game->timer = ElapsedMicroseconds.QuadPart; + game->tick = ElapsedMicroseconds.QuadPart / 1000; + if (!update_game_tick(game)) { + break; + } + Sleep(1); + } + return 0; +} +#else +DWORD WINAPI cetris_game_loop(void* data) { + cetris_game* game = (cetris_game*)data; + while(1) { + if (game->waiting) break; + game->tick += 16; + if (!update_game_tick(game)) { + break; + } + Sleep(16); // little less than 60hz + } +} +#endif +CETRIS_EXPORT void cetris_start_game(cetris_game *g) { + g->waiting = false; + HANDLE thread = CreateThread(NULL, 0, cetris_game_loop, g, 0, NULL); +} +CETRIS_EXPORT void cetris_stop_game(cetris_game *g) { + init_game(g, NULL); +} +#else +#include <pthread.h> +#include <unistd.h> + +#if CETRIS_HI_RES +#include <time.h> +void *cetris_game_loop(void* data) { + cetris_game *game = (cetris_game*)data; + struct timespec start_time, end_time; + clock_gettime(CLOCK_MONOTONIC_RAW, &start_time); + while (1) { + if (game->waiting) break; + clock_gettime(CLOCK_MONOTONIC_RAW, &end_time); + long nsec_elapsed = (end_time.tv_sec - start_time.tv_sec) * (long)1e9 + (end_time.tv_nsec - start_time.tv_nsec); + game->timer = nsec_elapsed / 1000; + game->tick = nsec_elapsed / 1000000; + if (!update_game_tick(game)) { + break; + } + usleep(1000); + } + return 0; +} +#else +void *cetris_game_loop(void *data) { + cetris_game* game = (cetris_game*)data; + while(1) { + if (game->waiting) break; + game->tick += 16; + if (!update_game_tick(game)) { + break; + } + // could be more accurate, keeping + // it consistant with windows + usleep(16000); + } +} +#endif +CETRIS_EXPORT void cetris_start_game(cetris_game *g) { + g->waiting = false; + pthread_t thread; + pthread_create(&thread, NULL, cetris_game_loop, (void*)g); +} +CETRIS_EXPORT void cetris_stop_game(cetris_game *g) { + init_game(g, NULL); +} +#endif diff --git a/lib/timer.h b/lib/timer.h new file mode 100644 index 0000000..2f29c84 --- /dev/null +++ b/lib/timer.h @@ -0,0 +1,4 @@ +#include <cetris.h> + +CETRIS_EXPORT void cetris_start_game(cetris_game *g); +CETRIS_EXPORT void cetris_stop_game(cetris_game *g); |