diff options
| author | 2019-10-28 15:49:51 -0400 | |
|---|---|---|
| committer | 2019-10-28 15:49:51 -0400 | |
| commit | d5ac165fdeb7b0c596a1d95845262b68f35e14d7 (patch) | |
| tree | eb05f1be1e52fd85f7a560ff4d485d73f42dbd6f | |
| 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
| -rw-r--r-- | frontends/curses/meson.build | 8 | ||||
| -rw-r--r-- | frontends/gl/meson.build | 12 | ||||
| -rw-r--r-- | frontends/sdl/audio/yea_0.wav | bin | 0 -> 212984 bytes | |||
| -rw-r--r-- | frontends/sdl/audio/yea_1.wav | bin | 0 -> 212984 bytes | |||
| -rw-r--r-- | frontends/sdl/audio/yea_2.wav | bin | 0 -> 212984 bytes | |||
| -rw-r--r-- | frontends/sdl/audio/yea_3.wav | bin | 0 -> 425912 bytes | |||
| -rw-r--r-- | frontends/sdl/meson.build | 18 | ||||
| -rw-r--r-- | frontends/sdl/sdl_ui.c | 315 | ||||
| -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 | ||||
| -rw-r--r-- | meson.build | 4 | ||||
| -rw-r--r-- | notes.txt | 2 | ||||
| -rwxr-xr-x | test/a.out | bin | 0 -> 16712 bytes | |||
| -rw-r--r-- | test/memory_test.c | 2 | ||||
| -rw-r--r-- | test/meson.build | 5 | ||||
| -rw-r--r-- | test/sdl_audio.c | 12 |
21 files changed, 1012 insertions, 746 deletions
diff --git a/frontends/curses/meson.build b/frontends/curses/meson.build index 6c7497f..0b89e88 100644 --- a/frontends/curses/meson.build +++ b/frontends/curses/meson.build @@ -1,7 +1,7 @@ src = ['curses_ui.c'] inc = [cetris_inc] -deps = [] +deps = [cetris] if host_machine.system() == 'windows' inc += include_directories('win') @@ -13,6 +13,6 @@ else endif executable('cetris', src, - dependencies: deps, - include_directories: inc, - install: false) + dependencies: deps, + include_directories: inc, + install: false) diff --git a/frontends/gl/meson.build b/frontends/gl/meson.build index 819011a..8b13c3c 100644 --- a/frontends/gl/meson.build +++ b/frontends/gl/meson.build @@ -1,7 +1,7 @@ src = ['main.c', 'glad/src/gl.c'] inc = [cetris_inc, include_directories('glad/include')] -deps = [] +deps = [cetris] if host_machine.system() == 'windows' deps += compiler.find_library('glfw3', dirs: meson.current_source_dir() + '/win/glfw') @@ -13,10 +13,10 @@ else endif configure_file(input: 'block.jpg', - output: 'block.jpg', - copy: true, install: false) + output: 'block.jpg', + copy: true, install: false) executable('cetris', src, - dependencies: deps, - include_directories: inc, - install: false) + dependencies: deps, + include_directories: inc, + install: false) diff --git a/frontends/sdl/audio/yea_0.wav b/frontends/sdl/audio/yea_0.wav Binary files differnew file mode 100644 index 0000000..6fa9fdf --- /dev/null +++ b/frontends/sdl/audio/yea_0.wav diff --git a/frontends/sdl/audio/yea_1.wav b/frontends/sdl/audio/yea_1.wav Binary files differnew file mode 100644 index 0000000..3c795ec --- /dev/null +++ b/frontends/sdl/audio/yea_1.wav diff --git a/frontends/sdl/audio/yea_2.wav b/frontends/sdl/audio/yea_2.wav Binary files differnew file mode 100644 index 0000000..67511b9 --- /dev/null +++ b/frontends/sdl/audio/yea_2.wav diff --git a/frontends/sdl/audio/yea_3.wav b/frontends/sdl/audio/yea_3.wav Binary files differnew file mode 100644 index 0000000..4098797 --- /dev/null +++ b/frontends/sdl/audio/yea_3.wav diff --git a/frontends/sdl/meson.build b/frontends/sdl/meson.build index 10ce2d0..1953c08 100644 --- a/frontends/sdl/meson.build +++ b/frontends/sdl/meson.build @@ -1,10 +1,8 @@ src = ['sdl_ui.c'] +deps = [cetris] inc = [cetris_inc] -threads = dependency('threads') -deps = [] - if host_machine.system() == 'windows' deps += compiler.find_library('SDL2', dirs: meson.current_source_dir() + '/win/SDL2-2.0.10/lib/x64') deps += compiler.find_library('SDL2_ttf', dirs: meson.current_source_dir() + '/win/SDL2_ttf-2.0.15/lib/x64') @@ -17,14 +15,14 @@ else endif configure_file(input: 'config.ini', - output: 'config.ini', - copy: true, install: false) + output: 'config.ini', + copy: true, install: false) configure_file(input: 'OpenSans-Regular.ttf', - output: 'OpenSans-Regular.ttf', - copy: true, install: false) + output: 'OpenSans-Regular.ttf', + copy: true, install: false) executable('cetris', src, - dependencies: deps, - include_directories: inc, - install: false) + dependencies: deps, + include_directories: inc, + install: false) diff --git a/frontends/sdl/sdl_ui.c b/frontends/sdl/sdl_ui.c index f674581..e5b371a 100644 --- a/frontends/sdl/sdl_ui.c +++ b/frontends/sdl/sdl_ui.c @@ -1,14 +1,10 @@ #define SDL_MAIN_HANDLED #ifdef _WIN32 #include <SDL.h> -#include <windows.h> -#include <profileapi.h> #include <SDL_ttf.h> #else #include <SDL2/SDL.h> #include <SDL2/SDL_ttf.h> -#include <pthread.h> -#include <unistd.h> #endif #include <math.h> #include <stdlib.h> @@ -18,6 +14,8 @@ #include <time.h> #include <cetris.h> +#include <timer.h> +#include <rules.h> #include <ini.h> #define W 570 @@ -29,22 +27,32 @@ #define Y_OFFSET (H/2) - (BLOCK_SIZE * 10) SDL_Renderer* render; -TTF_Font* font; +TTF_Font* normal_font; +TTF_Font* big_font; SDL_Window *window; SDL_Surface *screen; +bool show_menu; +int menu_index; + bool dark_mode; SDL_Color main_color; SDL_Color off; SDL_Color text; float count_down; -bool game_running; - -long long timer; cetris_game g; +typedef struct { + SDL_AudioSpec wav_spec; + uint32_t wav_length; + uint8_t *wav_buffer; +} audio_clip; + +audio_clip clear_sound[4]; +SDL_AudioDeviceID audio_device; + uint8_t colors[7][3] = { {253,253,150}, // Yellow {174,198,207}, // Aqua @@ -55,59 +63,38 @@ uint8_t colors[7][3] = { {177,156,217} // Purple }; -#ifdef _WIN32 -DWORD WINAPI game_loop(void* data) { - LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds; - LARGE_INTEGER Frequency; - QueryPerformanceFrequency(&Frequency); - QueryPerformanceCounter(&StartingTime); - while(1) { - if (!game_running) break; - QueryPerformanceCounter(&EndingTime); - ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart; - ElapsedMicroseconds.QuadPart *= 1000000; - ElapsedMicroseconds.QuadPart /= Frequency.QuadPart; - timer = ElapsedMicroseconds.QuadPart; - g.tick = ElapsedMicroseconds.QuadPart / 1000; - if (!update_game_tick(&g)) { - break; - } - Sleep(1); - } - return 0; -} -#else -void *game_loop(void) { - timer = 0; - long nsec = 1000; - while(1) { - if (timer % 1000 == 0) { - g.tick++; - update_game_tick(&g); - } - nanosleep((const struct timespec[]){{0, nsec}}, NULL); - timer++; - } - return 0; -} -#endif - void setup() { SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO); window = SDL_CreateWindow("cetris", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, W, H, SDL_WINDOW_SHOWN); screen = SDL_GetWindowSurface(window); - //SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); render = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC|SDL_RENDERER_ACCELERATED); SDL_RenderSetLogicalSize(render, W, H); if (!render) exit(fprintf(stderr, "err: could not create SDL renderer\n")); TTF_Init(); - font = TTF_OpenFont("OpenSans-Regular.ttf", 20); - //TTF_SetFontHinting(font, TTF_HINTING_MONO); + normal_font = TTF_OpenFont("OpenSans-Regular.ttf", 20); + big_font = TTF_OpenFont("OpenSans-Regular.ttf", 30); + TTF_SetFontHinting(normal_font, TTF_HINTING_MONO); + TTF_SetFontHinting(big_font, TTF_HINTING_MONO); + + for (int i = 0; i < 4; i++) { + char name[25]; + sprintf(name, "audio/yea_%i.wav", i); + SDL_LoadWAV(name, &(clear_sound[i].wav_spec), &(clear_sound[i].wav_buffer), &(clear_sound[i].wav_length)); + } + audio_device = SDL_OpenAudioDevice(NULL, 0, &(clear_sound[0].wav_spec), NULL, 0); + if (audio_device == 0) printf("failed to open audio device\n"); } -void draw_text(char* string, int x, int y) { - SDL_Surface* surface = TTF_RenderText_Solid(font, string, text); +void draw_text(char* string, int x, int y, bool big) { + SDL_Surface *surface; + if (big) { + surface = TTF_RenderText_Solid(big_font, string, text); + } else { + surface = TTF_RenderText_Solid(normal_font, string, text); + } + SDL_Rect message; message.x = x; message.y = y; @@ -134,8 +121,33 @@ void load_colors() { } } +void draw_menu() { + SDL_Texture *m = SDL_CreateTexture(render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, W, H); + SDL_SetRenderTarget(render, m); + + SDL_SetRenderDrawColor(render, main_color.r, main_color.g, main_color.b, 255); + SDL_RenderClear(render); + + if (menu_index == 0) { + draw_text("> Play <", 80, (H / 2) - 30, true); + } else { + draw_text("Play", 80, (H / 2) - 30, true); + } + if (menu_index == 1) { + draw_text("> Config <", 80, (H / 2), true); + } else { + draw_text("Config", 80, (H / 2), true); + } + + SDL_SetRenderTarget(render, NULL); + + SDL_Point center = {W / 2, H / 2}; + SDL_RenderCopyEx(render, m, NULL, NULL, 0, ¢er, SDL_FLIP_NONE); + + SDL_DestroyTexture(m); +} -void draw() { +void draw_game() { SDL_Texture *m = SDL_CreateTexture(render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, W, H); SDL_SetRenderTarget(render, m); @@ -153,12 +165,16 @@ void draw() { SDL_RenderDrawRect(render, &queue); SDL_RenderDrawRect(render, &hold); + int BOARD_Y = g.config.board_y; + int BOARD_X = g.config.board_x; + int BOARD_VISIBLE = g.config.board_y - g.config.board_visible; + SDL_SetRenderDrawColor(render, main_color.r, main_color.g, main_color.b, 255); - for (int x = 0; x < CETRIS_BOARD_X + 1; x++) { + for (int x = 0; x < BOARD_X + 1; x++) { int rx = X_OFFSET + 1 + (x * BLOCK_SIZE); SDL_RenderDrawLine(render, rx, Y_OFFSET + 1, rx, Y_OFFSET + 500); } - for (int y = 0; y < CETRIS_BOARD_Y - CETRIS_BOARD_VISABLE + 1; y++) { + for (int y = 0; y < BOARD_Y - BOARD_VISIBLE + 1; y++) { int ry = Y_OFFSET + (y * BLOCK_SIZE); SDL_RenderDrawLine(render, X_OFFSET + 1, ry, X_OFFSET + 250, ry); } @@ -170,7 +186,7 @@ void draw() { for (int x = 0; x < 4; x++) { if ((g.current.m[y]>>(3 - x))&1) { b.x = X_OFFSET + ((x + g.current.pos.x) * BLOCK_SIZE) + 1; - b.y = (Y_OFFSET + ((y + g.current.pos.y) * BLOCK_SIZE)) - (CETRIS_BOARD_VISABLE * BLOCK_SIZE); + b.y = (Y_OFFSET + ((y + g.current.pos.y) * BLOCK_SIZE)) - (BOARD_VISIBLE * BLOCK_SIZE); uint8_t (*color)[3] = &(colors[g.current.t]); SDL_SetRenderDrawColor(render, (*color)[0], (*color)[1], (*color)[2], 255); @@ -183,7 +199,7 @@ void draw() { w.x = b.x - 1; SDL_RenderDrawRect(render, &w); - b.y = (Y_OFFSET + ((y + g.current.ghost_y) * BLOCK_SIZE)) - (CETRIS_BOARD_VISABLE * BLOCK_SIZE); + b.y = (Y_OFFSET + ((y + g.current.ghost_y) * BLOCK_SIZE)) - (BOARD_VISIBLE * BLOCK_SIZE); w.y = b.y - 1; SDL_RenderDrawRect(render, &w); @@ -253,11 +269,11 @@ void draw() { } } - for (int x = 0; x < CETRIS_BOARD_X; x++) { - for (int y = g.highest_line; y < CETRIS_BOARD_Y; y++) { + for (int x = 0; x < BOARD_X; x++) { + for (int y = g.highest_line; y < BOARD_Y; y++) { if (g.board[x][y] & SLOT_OCCUPIED) { b.x = X_OFFSET + (x * BLOCK_SIZE) + 1; - b.y = (Y_OFFSET + (y * BLOCK_SIZE)) - (CETRIS_BOARD_VISABLE * BLOCK_SIZE); + b.y = (Y_OFFSET + (y * BLOCK_SIZE)) - (BOARD_VISIBLE * BLOCK_SIZE); if (g.line_remove_tick[y]) { if ((int)g.tick % 2 == 0) { @@ -280,7 +296,7 @@ void draw() { } char *buf = malloc(200); - long double second = timer / 1000000.0f; + long double second = g.timer / 1000000.0f; if (second > 60.0f) { int minute = (int)(second / 60.0f); second -= (minute * 60.0f); @@ -288,10 +304,10 @@ void draw() { } else { sprintf(buf, "%.6Lf", second); } - draw_text(buf, 20, H - 40); + draw_text(buf, 20, H - 40, false); sprintf(buf, "lines remaining: %i", 20 - g.lines); - draw_text(buf, 20, H - 60); + draw_text(buf, 20, H - 60, false); free(buf); @@ -307,23 +323,13 @@ void draw() { } } -void start_game() { - g.waiting = false; -#ifdef _WIN32 - HANDLE thread = CreateThread(NULL, 0, game_loop, NULL, 0, NULL); -#else - pthread_t thread; - pthread_create(&thread, NULL, (void*)game_loop, (void*)0); -#endif -} - int main(void) { setup(); ini_parser p; load_ini_file(&p, "config.ini"); - cetris_config config; + cetris_config config = tetris_ds_config; int das = atoi(get_ini_value(&p, "das", "das")); config.das_das = das; @@ -331,78 +337,125 @@ int main(void) { int arr = atoi(get_ini_value(&p, "das", "arr")); config.das_arr = arr; - config.drop_period = atoi(get_ini_value(&p, "game", "drop_delay")); - config.next_piece_delay = atoi(get_ini_value(&p, "game", "next_piece_delay")); - config.lock_delay = atoi(get_ini_value(&p, "game", "lock_delay")); - config.force_lock = atoi(get_ini_value(&p, "game", "force_lock")); - config.wait_on_clear = atoi(get_ini_value(&p, "game", "wait_on_clear")); - config.line_delay_clear = atoi(get_ini_value(&p, "game", "line_clear_delay")); + config.win_condition = twenty_line_sprint; + config.levels = &tetris_worlds_levels[0]; + config.wait_on_clear = 0; + + char* drop_delay = get_ini_value(&p, "game", "drop_delay"); + if (drop_delay) config.drop_period = atoi(drop_delay); + + char* next_piece_delay = get_ini_value(&p, "game", "next_piece_delay"); + if (next_piece_delay) config.next_piece_delay = atoi(next_piece_delay); + + char* lock_delay = get_ini_value(&p, "game", "lock_delay"); + if (lock_delay) config.lock_delay = atoi(lock_delay); + + char* force_lock = get_ini_value(&p, "game", "force_lock"); + if (force_lock) config.force_lock = atoi(force_lock); + + char* wait_on_clear = get_ini_value(&p, "game", "wait_on_clear"); + if (wait_on_clear) config.wait_on_clear = atoi(wait_on_clear); + + char* line_delay_clear = get_ini_value(&p, "game", "line_clear_delay"); + if (line_delay_clear) config.line_delay_clear = atoi(line_delay_clear); + + char *dark = get_ini_value(&p, "ui", "dark_mode"); + if (dark) dark_mode = atoi(dark); + else dark_mode = 0; - dark_mode = atoi(get_ini_value(&p, "ui", "dark_mode")); load_colors(); - + + show_menu = false; + menu_index = 0; + count_down = 3; init_game(&g, &config); - g.waiting = true; SDL_Event e; for(;;) { while(SDL_PollEvent(&e)) { - switch (e.type) { - case SDL_QUIT: - exit(0); - case SDL_KEYDOWN: - switch (e.key.keysym.sym) { - case SDLK_LEFT: - move_piece(&g, LEFT); break; - case SDLK_RIGHT: - move_piece(&g, RIGHT); break; - case SDLK_DOWN: - move_piece(&g, DOWN); break; - case SDLK_SPACE: - move_piece(&g, HARD_DROP); break; - case SDLK_UP: - move_piece(&g, ROTATE_CW); break; - case 'c': - hold_piece(&g); break; - case 'x': - move_piece(&g, ROTATE_CW); break; - case 'z': - move_piece(&g, ROTATE_CCW); break; - case 'r': - game_running = false; - init_game(&g, &config); - g.waiting = true; - count_down = 3; - break; - } - break; - case SDL_KEYUP: - switch (e.key.keysym.sym) { - case SDLK_LEFT: - unhold_piece(&g, LEFT); break; - case SDLK_RIGHT: - unhold_piece(&g, RIGHT); break; - case SDLK_DOWN: - unhold_piece(&g, DOWN); break; - case SDLK_SPACE: - unhold_piece(&g, HARD_DROP); break; - case SDLK_UP: - unhold_piece(&g, ROTATE_CW); break; - case 'x': - unhold_piece(&g, ROTATE_CW); break; - case 'z': - unhold_piece(&g, ROTATE_CCW); break; - } + if (show_menu) { + switch (e.type) { + case SDL_QUIT: + exit(0); + case SDL_KEYDOWN: + switch (e.key.keysym.sym) { + case SDLK_DOWN: + if (menu_index < 1) menu_index++; + break; + case SDLK_UP: + if (menu_index > 0) menu_index--; + break; + } + } + } else { + switch (e.type) { + case SDL_QUIT: + exit(0); + case SDL_KEYDOWN: + switch (e.key.keysym.sym) { + case SDLK_LEFT: + move_piece(&g, LEFT); break; + case SDLK_RIGHT: + move_piece(&g, RIGHT); break; + case SDLK_DOWN: + move_piece(&g, DOWN); break; + case SDLK_SPACE: + move_piece(&g, HARD_DROP); break; + case SDLK_UP: + move_piece(&g, ROTATE_CW); break; + case 'c': + hold_piece(&g); break; + case 'x': + move_piece(&g, ROTATE_CW); break; + case 'z': + move_piece(&g, ROTATE_CCW); break; + case 'r': + cetris_stop_game(&g); + count_down = 3; + break; + } + break; + case SDL_KEYUP: + switch (e.key.keysym.sym) { + case SDLK_LEFT: + unhold_move(&g, LEFT); break; + case SDLK_RIGHT: + unhold_move(&g, RIGHT); break; + case SDLK_DOWN: + unhold_move(&g, DOWN); break; + case SDLK_SPACE: + unhold_move(&g, HARD_DROP); break; + case SDLK_UP: + unhold_move(&g, ROTATE_CW); break; + case 'x': + unhold_move(&g, ROTATE_CW); break; + case 'z': + unhold_move(&g, ROTATE_CCW); break; + } + } } } - draw(); - if (count_down < 0 && !game_running) { - start_game(); - game_running = true; + if (show_menu) { + draw_menu(); + } else { + draw_game(); + } + + if (count_down < 0 && g.waiting) { + cetris_start_game(&g); } SDL_RenderPresent(render); + + if (g.line_event) { + int index = g.line_combo - 1; + if (index > 4) index = 4; + int success = SDL_QueueAudio(audio_device, clear_sound[index].wav_buffer, clear_sound[index].wav_length); + SDL_PauseAudioDevice(audio_device, 0); + g.line_event = false; + } + SDL_Delay(1000 / 60); } 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); diff --git a/meson.build b/meson.build index 10ac21f..12c5d6b 100644 --- a/meson.build +++ b/meson.build @@ -12,7 +12,11 @@ if get_option('debug') == true else add_global_arguments('-DBUILD_TESTS', language : 'c') endif +endif + +subdir('lib') +if get_option('debug') == true subdir('test') endif @@ -14,6 +14,8 @@ change some enums and make piece_matrix samller: 606 bytes optimize piece_queue: 530 bytes +add das: 936 bytes + 000 = 0 001 = 1 010 = 2 diff --git a/test/a.out b/test/a.out Binary files differnew file mode 100755 index 0000000..bd6f439 --- /dev/null +++ b/test/a.out diff --git a/test/memory_test.c b/test/memory_test.c index 4b00aab..8966a4f 100644 --- a/test/memory_test.c +++ b/test/memory_test.c @@ -1,6 +1,6 @@ #include <stdio.h> -#include "cetris.h" +#include <cetris.h> int main(void) { cetris_game game; diff --git a/test/meson.build b/test/meson.build index 351be02..f5b9ae4 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,5 +1,6 @@ mem_test = ['memory_test.c'] executable('mem_test', mem_test, - include_directories: cetris_inc, - install: false) + dependencies: cetris, + include_directories: cetris_inc, + install: false) diff --git a/test/sdl_audio.c b/test/sdl_audio.c new file mode 100644 index 0000000..53f2514 --- /dev/null +++ b/test/sdl_audio.c @@ -0,0 +1,12 @@ +#include <SDL2/SDL.h> + +int main(void) { + int i, count = SDL_GetNumAudioDevices(0); + + printf("count: %i\n", count); + for (i = 0; i < count; ++i) { + SDL_Log("Audio device %d: %s", i, SDL_GetAudioDeviceName(i, 0)); + printf("Audio device %d: %s", i, SDL_GetAudioDeviceName(i, 0)); + } + return 0; +} |