summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Opalach <andrew@akon.city> 2019-10-28 15:49:51 -0400
committerAndrew Opalach <andrew@akon.city> 2019-10-28 15:49:51 -0400
commitd5ac165fdeb7b0c596a1d95845262b68f35e14d7 (patch)
treeeb05f1be1e52fd85f7a560ff4d485d73f42dbd6f
parent223b6c74f5534fa64b2b0f7d638619c5cb7a0342 (diff)
downloadcetris-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.build8
-rw-r--r--frontends/gl/meson.build12
-rw-r--r--frontends/sdl/audio/yea_0.wavbin0 -> 212984 bytes
-rw-r--r--frontends/sdl/audio/yea_1.wavbin0 -> 212984 bytes
-rw-r--r--frontends/sdl/audio/yea_2.wavbin0 -> 212984 bytes
-rw-r--r--frontends/sdl/audio/yea_3.wavbin0 -> 425912 bytes
-rw-r--r--frontends/sdl/meson.build18
-rw-r--r--frontends/sdl/sdl_ui.c315
-rw-r--r--lib/cetris.c586
-rw-r--r--lib/cetris.h638
-rw-r--r--lib/meson.build11
-rw-r--r--lib/rules.c40
-rw-r--r--lib/rules.h7
-rw-r--r--lib/timer.c94
-rw-r--r--lib/timer.h4
-rw-r--r--meson.build4
-rw-r--r--notes.txt2
-rwxr-xr-xtest/a.outbin0 -> 16712 bytes
-rw-r--r--test/memory_test.c2
-rw-r--r--test/meson.build5
-rw-r--r--test/sdl_audio.c12
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
new file mode 100644
index 0000000..6fa9fdf
--- /dev/null
+++ b/frontends/sdl/audio/yea_0.wav
Binary files differ
diff --git a/frontends/sdl/audio/yea_1.wav b/frontends/sdl/audio/yea_1.wav
new file mode 100644
index 0000000..3c795ec
--- /dev/null
+++ b/frontends/sdl/audio/yea_1.wav
Binary files differ
diff --git a/frontends/sdl/audio/yea_2.wav b/frontends/sdl/audio/yea_2.wav
new file mode 100644
index 0000000..67511b9
--- /dev/null
+++ b/frontends/sdl/audio/yea_2.wav
Binary files differ
diff --git a/frontends/sdl/audio/yea_3.wav b/frontends/sdl/audio/yea_3.wav
new file mode 100644
index 0000000..4098797
--- /dev/null
+++ b/frontends/sdl/audio/yea_3.wav
Binary files differ
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, &center, 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
diff --git a/notes.txt b/notes.txt
index 847d115..213d3c3 100644
--- a/notes.txt
+++ b/notes.txt
@@ -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
new file mode 100755
index 0000000..bd6f439
--- /dev/null
+++ b/test/a.out
Binary files differ
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;
+}