summaryrefslogtreecommitdiff
path: root/frontends
diff options
context:
space:
mode:
Diffstat (limited to 'frontends')
-rw-r--r--frontends/curses/curses_ui.c258
-rw-r--r--frontends/curses/meson.build17
-rw-r--r--frontends/sdl/cetris_sdl.c727
-rw-r--r--frontends/sdl/cetris_sdl.h112
-rw-r--r--frontends/sdl/config.ini15
-rw-r--r--frontends/sdl/data.zipbin0 -> 1663021 bytes
-rw-r--r--frontends/sdl/meson.build14
-rw-r--r--frontends/timer.c86
8 files changed, 1229 insertions, 0 deletions
diff --git a/frontends/curses/curses_ui.c b/frontends/curses/curses_ui.c
new file mode 100644
index 0000000..91792f5
--- /dev/null
+++ b/frontends/curses/curses_ui.c
@@ -0,0 +1,258 @@
+/* very simple curses interface, DAS is not functional because you cant detect
+ * key up presses on the terminal, there are 2 character sets, ASCII_COMPATIBLE
+ * for no UTF-8 chars and the normal which will look better on a platform that can
+ * support UTF-8. Color support is limited because I havent figured out colors yet */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdbool.h>
+#ifdef _WIN32
+#include <curses.h>
+#include <windows.h>
+#else
+#include <ncurses.h>
+#include <unistd.h>
+#include <pthread.h>
+#endif
+#include <locale.h>
+
+#define CETRIS_HI_RES 1
+#define CETRIS_ENABLE_DAS 0
+#include <cetris.h>
+#include <rules.h>
+
+#include "../timer.c"
+
+#define WIN_COL 55
+#define WIN_LINE 25
+
+//#define ASCII_COMPATIBLE
+#ifdef ASCII_COMPATIBLE
+#define BLOCK "[]"
+#define PLAY_FIELD_STR " /--------------------\\ /----------------\\ \n"\
+ " | | | | \n"\
+ " | | \\----------------/ \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | /---------\\ \n"\
+ " | | | | \n"\
+ " | | | | \n"\
+ " | | | | \n"\
+ " | | | | \n"\
+ " | | \\---------/ \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " | | \n"\
+ " \\--------------------/"
+#else
+#define BLOCK "[]"
+#define PLAY_FIELD_STR " ┏━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━score━━━━━┓ \n"\
+ " ┃ ┃ ┃ ┃ \n"\
+ " ┃ ┃ ┗━━━━━━━━━━━━━━━┛ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ ┏━━━queue━━━┓ \n"\
+ " ┃ ┃ ┃ ┃ \n"\
+ " ┃ ┃ ┃ ┃ \n"\
+ " ┃ ┃ ┃ ┃ \n"\
+ " ┃ ┃ ┃ ┃ \n"\
+ " ┃ ┃ ┗━━━━━━━━━━━┛ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┃ ┃ \n"\
+ " ┗━━━━━━━━━━━━━━━━━━━━┛ "
+#endif
+
+#define X_OFFSET 8
+#define Y_OFFSET -2
+
+ctrs_game game = { 0 };
+bool is_paused = false;
+
+void curses_init() {
+ setlocale(LC_CTYPE, "");
+ initscr();
+ curs_set(0);
+ noecho();
+ keypad(stdscr, TRUE);
+ timeout(2);
+
+#ifdef _WIN32 // only resize manually on windows
+ resize_term(WIN_LINE, WIN_COL);
+#endif
+
+ start_color();
+ init_pair(MINO_O, COLOR_YELLOW, COLOR_BLACK);
+ init_pair(MINO_Z, COLOR_RED, COLOR_BLACK);
+ init_pair(MINO_S, COLOR_GREEN, COLOR_BLACK);
+ init_pair(MINO_T, COLOR_MAGENTA, COLOR_BLACK);
+ init_pair(MINO_L, COLOR_WHITE, COLOR_BLACK); // should be orange
+ init_pair(MINO_I, COLOR_CYAN, COLOR_BLACK);
+ init_pair(MINO_J, COLOR_BLUE, COLOR_BLACK);
+ clear();
+}
+
+#ifdef _WIN32
+DWORD WINAPI game_loop(void* data) {
+ while(1) {
+ Sleep((1.0/60.0) * 1000.0);
+ if (!is_paused) ctrs_update_game_tick(&game);
+ }
+ return 0;
+}
+#else
+void *game_loop(void) {
+ struct timespec start_time, end_time;
+ clock_gettime(CLOCK_MONOTONIC_RAW, &start_time);
+ while (1) {
+ 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 (!ctrs_update_game_tick(&game)) {
+ break;
+ }
+ usleep(1000);
+ }
+ return 0;
+}
+#endif
+
+
+void draw_board() {
+ mvaddstr(0, 0, PLAY_FIELD_STR);
+
+ for (int y = 0; y < 4; y++) {
+ for (int x = 0; x < 4; x++) {
+ if ((game.current.m[y]>>(3 - x))&1) {
+ int draw_x = X_OFFSET + ((x + game.current.pos.x) * 2);
+ int draw_y = Y_OFFSET + (y + game.current.pos.y) - game.config.board_visible;
+ int ghost_y = Y_OFFSET + (y + game.current.ghost_y) - game.config.board_visible;
+ attron(A_DIM);
+ mvaddstr(ghost_y, draw_x, BLOCK);
+ attroff(A_DIM);
+ attron(COLOR_PAIR(game.current.t) | A_BOLD);
+ mvaddstr(draw_y, draw_x, BLOCK);
+ attroff(COLOR_PAIR(game.current.t) | A_BOLD);
+ }
+ if ((ctrs_default_matrices[game.piece_queue[game.current_index]][y]>>(3 - x))&1) {
+ mvaddstr(6 + y, (x * 2) + 36, BLOCK);
+ }
+ }
+ }
+
+ for (int x = 0; x < game.config.board_x; x++) {
+ for (int y = game.highest_line; y < game.config.board_y; y++) {
+ int draw_y = y - game.config.board_visible + Y_OFFSET;
+ int draw_x = x * 2 + X_OFFSET;
+ if (game.board[x][y] & CTRS_SLOT_OCCUPIED) {
+ attron(COLOR_PAIR(game.board[x][y] >> 5) | A_BOLD);
+ if (game.line_remove_tick[y]) {
+ if (game.tick % 2 == 0) {
+ mvaddstr(draw_y, draw_x, BLOCK);
+ }
+ } else {
+ mvaddstr(draw_y, draw_x, BLOCK);
+ }
+ attroff(COLOR_PAIR(game.board[x][y] >> 5) | A_BOLD);
+ }
+ }
+ }
+
+ attron(A_BOLD);
+
+ char score[50];
+ sprintf(score, "%i", game.score);
+ mvaddstr(1, (39 + X_OFFSET) - strlen(score), score);
+
+ char level[20];
+ sprintf(level, "%i", game.level);
+ mvaddstr(3, 37, "Level");
+ mvaddstr(4, 40 - strlen(level), level);
+
+ if (game.game_over) {
+ mvaddstr(10, 5 + X_OFFSET, "GAME OVER");
+ mvaddstr(11, 4 + X_OFFSET, "r to restart");
+ }
+
+ if (is_paused) {
+ mvaddstr(10, 7 + X_OFFSET, "paused");
+ }
+
+ attroff(A_BOLD);
+}
+
+int main(void) {
+ curses_init();
+
+ game.config = tetris_ds_config;
+ game.config.levels = &tetris_worlds_levels[0];
+ game.config.win_condition = twenty_line_sprint;
+ game.config.wait_on_clear = 0;
+ ctrs_init_game(&game);
+
+ ctrs_start_game(&game);
+ /*
+#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 c;
+ while(1) {
+ c = getch();
+ switch (c) {
+ case 'q': endwin(); exit(1);
+ case 27: // esc or alt
+ is_paused = !is_paused; break;
+ case 'r':
+ if (game.game_over) {
+ ctrs_init_game(&game);
+ }
+ break;
+ }
+ if (is_paused) continue; // dont allow input if paused
+ switch (c) {
+ case KEY_LEFT:
+ ctrs_move_piece(&game, LEFT); break;
+ case KEY_RIGHT:
+ ctrs_move_piece(&game, RIGHT); break;
+ case KEY_DOWN:
+ ctrs_move_piece(&game, DOWN); break;
+ case KEY_UP:
+ case 'x':
+ ctrs_move_piece(&game, ROTATE_CW); break;
+ case '^':
+ case 'z':
+ ctrs_move_piece(&game, ROTATE_CCW); break;
+ case ' ':
+ ctrs_move_piece(&game, HARD_DROP); break;
+ case KEY_SLEFT:
+ case 'c':
+ ctrs_hold_piece(&game); break;
+ }
+ erase();
+ draw_board();
+ refresh();
+ }
+ return 0;
+}
+
diff --git a/frontends/curses/meson.build b/frontends/curses/meson.build
new file mode 100644
index 0000000..46763fd
--- /dev/null
+++ b/frontends/curses/meson.build
@@ -0,0 +1,17 @@
+src = ['curses_ui.c']
+
+inc = [cetris_inc]
+deps = []
+
+if host_machine.system() == 'windows'
+ deps += compiler.find_library('pdcurses')
+else
+ deps += dependency('ncursesw')
+ deps += dependency('tinfow')
+ deps += dependency('threads')
+endif
+
+executable('cetris', src,
+ dependencies: deps,
+ include_directories: inc,
+ install: false)
diff --git a/frontends/sdl/cetris_sdl.c b/frontends/sdl/cetris_sdl.c
new file mode 100644
index 0000000..3bb6cb8
--- /dev/null
+++ b/frontends/sdl/cetris_sdl.c
@@ -0,0 +1,727 @@
+#define _XOPEN_SOURCE 500
+
+#define CETRIS_HI_RES 1
+#define CETRIS_ENABLE_DAS 1
+#include <cetris.h>
+#include <rules.h>
+#include "../../ini/ini.h"
+
+#include "../timer.c"
+
+#define SDL_MAIN_HANDLED
+#include <SDL2/SDL.h>
+#include <SDL2/SDL_ttf.h>
+#include <SDL2/SDL_image.h>
+#ifdef _WIN32
+#define format_str sprintf_s
+#else
+#define format_str snprintf
+#endif
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include "cetris_sdl.h"
+
+#define W 900
+#define H 720
+#define FRAME_RATE 60
+
+void setup_sdl(cetris_ui *ui) {
+ SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO);
+ ui->window = SDL_CreateWindow("cetris", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, W, H, SDL_WINDOW_SHOWN);
+ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
+ SDL_SetHint(SDL_HINT_RENDER_DRIVER,"opengl");
+ ui->render = SDL_CreateRenderer(ui->window, -1, SDL_RENDERER_PRESENTVSYNC|SDL_RENDERER_ACCELERATED);
+ SDL_SetRenderDrawBlendMode(ui->render, SDL_BLENDMODE_BLEND);
+ SDL_RenderSetLogicalSize(ui->render, W, H);
+ if (!ui->render) exit(fprintf(stderr, "err: could not create SDL renderer\n"));
+
+ TTF_Init();
+ ui->font_count = 0;
+
+ IMG_Init(IMG_INIT_PNG);
+
+ SDL_AudioSpec spec;
+
+ SDL_memset(&spec, 0, sizeof(spec));
+ spec.freq = 44100;
+ spec.format = AUDIO_S16;
+ spec.channels = 2;
+ spec.samples = 4096;
+ spec.callback = NULL;
+
+ ui->audio_device = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
+ if (ui->audio_device == 0) printf("failed to open audio device\n");
+ else SDL_PauseAudioDevice(ui->audio_device, 0);
+}
+
+TTF_Font* get_font(cetris_ui *ui, int size) {
+ for (int i = 0; i < ui->font_count; i++) {
+ if (ui->fonts[i].size == size) {
+ return ui->fonts[i].font;
+ }
+ }
+ ui->fonts[ui->font_count].font = TTF_OpenFont("data/OpenSans-Regular.ttf", size);
+ TTF_SetFontHinting(ui->fonts[ui->font_count].font, TTF_HINTING_MONO);
+ ui->fonts[ui->font_count].size = size;
+ return ui->fonts[ui->font_count++].font;
+}
+
+SDL_Texture* load_png(cetris_ui* ui, char* file) {
+ SDL_Surface* loaded_surface = IMG_Load(file);
+ if (loaded_surface == NULL) {
+ fprintf(stderr, "err: failed to load %s\n", file);
+ return NULL;
+ }
+ return SDL_CreateTextureFromSurface(ui->render, loaded_surface);
+}
+
+uint8_t *lock_sound;
+uint32_t lock_sound_length;
+void load_multiple_audio(char* name, char* dir_name, int *count, audio_clip_t* list) {
+ char file[120];
+
+ int index = 0;
+ for (int i = 0; i < (*count); i++) {
+ format_str(file, 120, "%s/%s_%i.wav", dir_name, name, i);
+
+ if (SDL_LoadWAV(file, &list[index].wav_spec,
+ &list[index].wav_buffer, &list[index].wav_length) != NULL) {
+ SDL_MixAudioFormat(list[index].wav_buffer, lock_sound, list[index].wav_spec.format, lock_sound_length, SDL_MIX_MAXVOLUME);
+ index++;
+ } else (*count)--;
+ }
+}
+
+void load_skin(cetris_ui *ui) {
+ char *dir_name = malloc(100);
+ format_str(dir_name, 100, "skins/%s", ui->config.skin_name);
+
+ struct stat sb;
+
+#ifdef _WIN32
+ if (!(stat(dir_name, &sb) == 0 && (sb.st_mode & S_IFDIR))) {
+#else
+ if (!(stat(dir_name, &sb) == 0 && S_ISDIR(sb.st_mode))) {
+#endif
+ dir_name = "skins/default";
+ }
+
+ ini_parser p;
+
+ char ini_path[100];
+ format_str(ini_path, 100, "%s/skin.ini", dir_name);
+
+ char name[120];
+ format_str(name, 120, "%s/lock.wav", dir_name);
+ SDL_LoadWAV(name, &ui->skin.lock_sound.wav_spec,
+ &ui->skin.lock_sound.wav_buffer, &ui->skin.lock_sound.wav_length);
+ lock_sound = ui->skin.lock_sound.wav_buffer;
+ lock_sound_length = ui->skin.lock_sound.wav_length;
+
+ if (load_ini_file(&p, ini_path)) {
+ char* random_clear = get_ini_value(&p, "sounds", "random_clear_sound");
+ if (random_clear && !strcmp(random_clear, "true")) {
+ ui->skin.random_audio = true;
+ } else ui->skin.random_audio = false;
+
+ format_str(name, 120, "%s/playboard.png", dir_name);
+ ui->skin.game_background = load_png(ui, name);
+ if (ui->skin.game_background != NULL) {
+ SDL_SetTextureAlphaMod(ui->skin.game_background, 120);
+ SDL_SetTextureBlendMode(ui->skin.game_background, SDL_BLENDMODE_BLEND);
+ }
+
+ format_str(name, 120, "%s/overlay_middle.png", dir_name);
+ ui->skin.overlay_middle = load_png(ui, name);
+ if (ui->skin.overlay_middle != NULL) {
+ SDL_SetTextureBlendMode(ui->skin.overlay_middle, SDL_BLENDMODE_BLEND);
+ }
+
+ format_str(name, 120, "%s/border.png", dir_name);
+ ui->skin.border = load_png(ui, name);
+ if (ui->skin.border != NULL) {
+ SDL_SetTextureBlendMode(ui->skin.border, SDL_BLENDMODE_BLEND);
+ }
+
+ format_str(name, 120, "%s/background.png", dir_name);
+ ui->skin.background = load_png(ui, name);
+ if (ui->skin.border != NULL) {
+ SDL_SetTextureBlendMode(ui->skin.background, SDL_BLENDMODE_BLEND);
+ }
+
+ format_str(name, 120, "%s/blocks.png", dir_name);
+ ui->skin.blocks = load_png(ui, name);
+ if (ui->skin.blocks != NULL) {
+ SDL_SetTextureBlendMode(ui->skin.blocks, SDL_BLENDMODE_BLEND);
+ }
+
+ char* clear_count = get_ini_value(&p, "sounds", "clear_sound_count");
+ if (clear_count) {
+ ui->skin.clear_sound_count = atoi(clear_count);
+ free(clear_count);
+ }
+
+ if (ui->skin.clear_sound_count > 0) {
+ ui->skin.clear_sound =
+ (audio_clip_t *)malloc(sizeof(audio_clip_t) * ui->skin.clear_sound_count);
+ load_multiple_audio("clear", dir_name,
+ &ui->skin.clear_sound_count, ui->skin.clear_sound);
+ }
+
+ char* tetris_count = get_ini_value(&p, "sounds", "four_clear_sound_count");
+ if (tetris_count) {
+ ui->skin.tetris_sound_count = atoi(tetris_count);
+ free(tetris_count);
+ }
+
+ if (ui->skin.tetris_sound_count > 0) {
+ ui->skin.tetris_sound =
+ (audio_clip_t *)malloc(sizeof(audio_clip_t) * ui->skin.tetris_sound_count);
+ load_multiple_audio("four_clear", dir_name,
+ &ui->skin.tetris_sound_count, ui->skin.tetris_sound);
+ }
+ }
+}
+
+void draw_text(cetris_ui *ui, char* string, int x, int y, TTF_Font* font, SDL_Color color) {
+ SDL_Surface *surface;
+ surface = TTF_RenderText_Solid(font, string, color);
+ if (!surface) return;
+
+ SDL_Rect message;
+ message.x = x;
+ message.y = y;
+ message.w = surface->w;
+ message.h = surface->h;
+ SDL_Texture* tex = SDL_CreateTextureFromSurface(ui->render, surface);
+
+ SDL_RenderCopy(ui->render, tex, NULL, &message);
+ SDL_DestroyTexture(tex);
+ SDL_FreeSurface(surface);
+}
+
+void draw_image(cetris_ui *ui, SDL_Texture *i, int x, int y, int width, int height) {
+ SDL_Rect dest = {x, y, width, height};
+ SDL_RenderCopy(ui->render, i, NULL, &dest);
+}
+
+void draw_block(cetris_ui *ui, int x, int y, int width, int height, int mino, int alpha, SDL_Color off) {
+ if (ui->skin.blocks) {
+ SDL_Rect b = {x, y, width, height};
+ SDL_Rect block = {(mino + 2) * 32, 0, 32, 32};
+ SDL_SetTextureAlphaMod(ui->skin.blocks, alpha);
+ SDL_RenderCopy(ui->render, ui->skin.blocks, &block, &b);
+ } else {
+ SDL_Rect b = {x + 1, y + 1, width - 1, height - 1};
+ SDL_Color c = mino_colors[mino];
+ SDL_SetRenderDrawColor(ui->render, c.r, c.g, c.b, alpha);
+ SDL_RenderFillRect(ui->render, &b);
+ SDL_RenderDrawRect(ui->render, &b);
+ SDL_SetRenderDrawColor(ui->render, off.r, off.g, off.b, off.a);
+ b.y--; b.x--; b.w+=2; b.h+=2;
+ SDL_RenderDrawRect(ui->render, &b);
+ }
+}
+
+void draw_board(cetris_ui *ui, SDL_Texture *m, game_board_t* board, int x, int y, int w, int h) {
+ SDL_SetRenderTarget(ui->render, m);
+ SDL_RenderClear(ui->render);
+
+ int y_offset = 10;
+
+ SDL_Rect background = {0, 0, w, h + y_offset};
+
+ if (ui->skin.game_background) {
+ SDL_RenderCopy(ui->render, ui->skin.game_background, NULL, &background);
+ if (board->count_down > 0) {
+ SDL_SetRenderDrawColor(ui->render,
+ board->scheme.off.r - (int)(fmod((double)board->count_down, (double)1.0) * 50),
+ board->scheme.off.g, board->scheme.off.b, 170);
+
+ SDL_RenderFillRect(ui->render, &background);
+ SDL_RenderDrawRect(ui->render, &background);
+ }
+ } else {
+ SDL_SetRenderDrawColor(ui->render,
+ board->scheme.off.r - (int)(fmod((double)board->count_down, (double)1.0) * 50),
+ board->scheme.off.g, board->scheme.off.b, board->scheme.off.a);
+
+ SDL_RenderFillRect(ui->render, &background);
+ SDL_RenderDrawRect(ui->render, &background);
+ }
+
+ SDL_SetRenderDrawColor(ui->render, board->scheme.main.r,
+ board->scheme.main.g, board->scheme.main.b, board->scheme.main.a);
+
+
+ int board_x = board->game.config.board_x;
+ int board_y = board->game.config.board_y;
+ int board_visible = board_y - board->game.config.board_visible;
+
+ int block_width = w / board->game.config.board_x;
+ int block_height = h / board->game.config.board_visible;
+
+ if (ui->skin.overlay_middle) {
+ for (int j = 0; j < board->game.config.board_visible + 1; j++) {
+ if (j == 0) {
+ SDL_Rect overlay = {0, y_offset - block_height, (block_width * 10), block_height};
+ //SDL_Rect cutoff = {0, block_height - y_offset, (block_width * 10), block_height};
+ SDL_RenderCopy(ui->render, ui->skin.overlay_middle, NULL, &overlay);
+ } else {
+ SDL_Rect overlay = {0, y_offset + ((j - 1) * block_height), (block_width * 10), block_height};
+ SDL_RenderCopy(ui->render, ui->skin.overlay_middle, NULL, &overlay);
+ }
+ }
+ } else {
+ for (int s = 0; s < board_x + 1; s++) {
+ int rx = (s * block_width);
+ SDL_RenderDrawLine(ui->render, rx, 1, rx, h + y_offset);
+ }
+
+ for (int j = 0; j < board->game.config.board_visible + 1; j++) {
+ int ry = y_offset + (j * block_height);
+ SDL_RenderDrawLine(ui->render, 0, ry, w, ry);
+ }
+ }
+
+ for (int s = board->game.highest_line; s < board_y; s++) {
+ for (int j = 0; j < board_x; j++) {
+ if (board->game.board[j][s] & CTRS_SLOT_OCCUPIED) {
+ if (board->game.line_remove_tick[s]) {
+ if (board->game.tick % 2 == 0) {
+ continue;
+ }
+ }
+
+ int block_x = (j * block_width);
+ int block_y = (y_offset + ((s - board_visible) * block_height));
+
+ draw_block(ui, block_x, block_y, block_width, block_height,
+ (board->game.board[j][s] >> 5), 255, board->scheme.off);
+ }
+ }
+ }
+
+ if (!board->game.game_over) {
+ for (int s = 0; s < 4; s++) {
+ for (int j = 0; j < 4; j++) {
+ if ((board->game.current.m[s]>>(3 - j))&1) {
+ int block_x = ((j + board->game.current.pos.x) * block_width);
+ int block_y = y_offset + ((s + board->game.current.pos.y) - board_visible) * block_height;
+
+ draw_block(ui, block_x, block_y, block_width, block_height,
+ board->game.current.t, 255, board->scheme.off);
+
+ int ghost_y = y_offset + ((s + board->game.current.ghost_y) - board_visible) * block_height;
+ if (ghost_y != block_y) {
+ draw_block(ui, block_x, ghost_y, block_width, block_height,
+ board->game.current.t, 120, board->scheme.off);
+ }
+ }
+ }
+ }
+ }
+
+ if (board->count_down > 0)
+ board->count_down-=(1.0f/FRAME_RATE);
+
+ SDL_SetRenderTarget(ui->render, NULL);
+
+ SDL_Rect dest = {x, y, w, h + y_offset};
+ SDL_RenderCopy(ui->render, m, NULL, &dest);
+
+ if (ui->skin.border) {
+ SDL_Rect border = {x - 110, y - 35, 380, 600};
+ SDL_RenderCopy(ui->render, ui->skin.border, NULL, &border);
+ }
+}
+
+void draw_held_piece(cetris_ui *ui, SDL_Texture *m, game_board_t* board, int x, int y, int w, int h) {
+ if (w < 8) return;
+ if (h < 8) return;
+
+ //SDL_SetRenderTarget(ui->render, m);
+ //SDL_RenderClear(ui->render);
+
+ int block_width = (w - 4) / 5;
+ int block_height = (h - 4) / 5;
+
+ // make sure blocks are square
+ if (block_width > block_height) {
+ block_width = block_height;
+ } else {
+ block_height = block_width;
+ }
+
+ SDL_SetRenderDrawColor(ui->render, board->scheme.off.r,
+ board->scheme.off.g, board->scheme.off.b, board->scheme.off.a);
+
+ //SDL_Rect hold = {0, 0, w, h};
+ //SDL_RenderFillRect(ui->render, &hold);
+ //SDL_RenderDrawRect(ui->render, &hold);
+
+ if (board->game.piece_held) {
+ for (int s = 0; s < 4; s++) {
+ for (int j = 0; j < 4; j++) {
+ if ((board->game.held.m[s]>>(3 - j))&1) {
+ int block_x = 2 + ((j) * block_width);
+ int block_y = (s * block_height);
+ if (board->game.held.t == MINO_I) {
+ block_x += (int)(block_width / 1.5f);
+ block_y += block_height;
+ } else {
+ block_x += block_width;
+ block_y += (int)(block_height);
+ }
+ if (board->game.held.t == MINO_O) {
+ block_x -= block_width / 2;
+ }
+
+ draw_block(ui, x + block_x, y + block_y, block_width, block_height,
+ board->game.held.t, 255, board->scheme.off);
+
+ }
+ }
+ }
+ }
+
+ //SDL_SetRenderTarget(ui->render, NULL);
+
+ //SDL_Rect dest = {x, y, w, h};
+ //SDL_RenderCopy(ui->render, m, NULL, &dest);
+}
+
+void draw_piece_queue(cetris_ui* ui, SDL_Texture *m, game_board_t* board, int x, int y, int w, int h) {
+ if (w < 8) return;
+ if (h < 32) return;
+
+ //SDL_SetRenderTarget(ui->render, m);
+
+ //SDL_RenderClear(ui->render);
+
+ int block_width = ((w - 4) / 4);
+ int block_height = ((h - 5) / 5) / 3;
+
+ // make sure blocks are square
+ if (block_width > block_height) {
+ block_width = block_height;
+ } else {
+ block_height = block_width;
+ }
+
+ //SDL_SetRenderDrawColor(ui->render, board->scheme.off.r,
+ // board->scheme.off.g, board->scheme.off.b, board->scheme.off.a);
+
+ //SDL_Rect queue = {0, 0, w, h};
+ //SDL_RenderFillRect(ui->render, &queue);
+ //SDL_RenderDrawRect(ui->render, &queue);
+
+ for (int i = 0; i < 5; i++) {
+ int index = (board->game.current_index + i);
+
+ uint8_t mino;
+ if (index <= 6) {
+ mino = board->game.piece_queue[index];
+ } else {
+ index = index % 7;
+ mino = board->game.next_queue[index];
+ }
+
+ for (int s = 0; s < 4; s++) {
+ for (int j = 0; j < 4; j++) {
+ if ((ctrs_default_matrices[mino][s]>>(3 - j))&1) {
+ int block_x = 2 + ((j) * block_width);
+ int block_y = (int)(h * (i/5.0)) + (s * block_height);
+ if (mino == MINO_I) {
+ block_y += block_height / 2;
+ } else if (mino != MINO_O) {
+ block_x += block_width / 2;
+ }
+
+ draw_block(ui, x + block_x, y + block_y, block_width, block_height,
+ mino, 255, board->scheme.off);
+
+ }
+ }
+ }
+ }
+
+ //SDL_SetRenderTarget(ui->render, NULL);
+
+ //SDL_Rect dest = {x, y, w, h};
+ //SDL_RenderCopy(ui->render, m, NULL, &dest);
+
+}
+
+void draw_timer(cetris_ui *ui, game_board_t *board, int x, int y) {
+ char *buf = malloc(50);
+ long double second = board->game.timer / 1000000.0f;
+ if (second > 60.0f) {
+ int minute = (int)(second / 60.0f);
+ second -= (minute * 60.0f);
+ format_str(buf, 50, "%02d:%09.6Lf", minute, second);
+ } else {
+ format_str(buf, 50, "%.6Lf", second);
+ }
+ draw_text(ui, buf, x, y, get_font(ui, 25), board->scheme.text);
+
+ //format_str(buf, 50, "lines remaining: %i", 20 - g.lines);
+ //draw_text(buf, 20, H - 60, false);
+
+ free(buf);
+}
+
+void draw(cetris_ui* ui) {
+ SDL_SetRenderDrawColor(ui->render, ui->colors.main.r,
+ ui->colors.main.g, ui->colors.main.b, ui->colors.main.a);
+
+ SDL_RenderClear(ui->render);
+
+ if (ui->skin.background) {
+ int w, h;
+ SDL_QueryTexture(ui->skin.background, NULL, NULL, &w, &h);
+
+ int ratio = w / W;
+ SDL_Rect back = {0, 0, ratio * W, ratio * H};
+ SDL_RenderCopy(ui->render, ui->skin.background, &back, NULL);
+ }
+
+ switch (ui->current_game) {
+ case SOLO:
+ //draw_image(ui, ui->solo.background, 0, 0, W, H);
+ draw_board(ui, ui->solo.main, &ui->solo.game_board, (W / 2) - 125, (H / 2) - 250, 250, 500);
+ draw_held_piece(ui, ui->solo.hold, &ui->solo.game_board, (W / 2) - 230, (H / 2) - 250, 100, 100);
+ draw_piece_queue(ui, ui->solo.queue, &ui->solo.game_board, (W / 2) + 150, (H / 2) - 250, 100, 450);
+ draw_timer(ui, &ui->solo.game_board, 20, 20);
+ break;
+ }
+
+ SDL_RenderPresent(ui->render);
+}
+
+void load_config(cetris_ui *ui, game_board_t *board) {
+
+ ui->config.keys = (key_bindings_t){
+ .key_down = SDLK_DOWN,
+ .key_right = SDLK_RIGHT,
+ .key_left = SDLK_LEFT,
+ .key_rotate_cw = SDLK_UP,
+ .key_rotate_ccw = 'x',
+ .key_drop = SDLK_SPACE,
+ .key_hold = 'c',
+ .key_restart = 'r'
+ };
+
+ board->game.config = tetris_ds_config;
+ board->game.config.levels = &tetris_worlds_levels[0];
+ board->game.config.win_condition = twenty_line_sprint;
+ board->game.config.wait_on_clear = 0;
+
+ ini_parser p;
+ if (load_ini_file(&p, "config.ini")) {
+ char* das = get_ini_value(&p, "das", "das");
+ if (das) {
+ board->game.config.das_das = atoi(das);
+ free(das);
+ }
+
+ char* arr = get_ini_value(&p, "das", "arr");
+ if (arr) {
+ board->game.config.das_arr = atoi(arr);
+ free(arr);
+ }
+
+ char* drop_delay = get_ini_value(&p, "game", "drop_delay");
+ if (drop_delay) {
+ board->game.config.drop_period = atoi(drop_delay);
+ free(drop_delay);
+ }
+
+ char* next_piece_delay = get_ini_value(&p, "game", "next_piece_delay");
+ if (next_piece_delay) {
+ board->game.config.next_piece_delay = atoi(next_piece_delay);
+ free(next_piece_delay);
+ }
+
+ char* lock_delay = get_ini_value(&p, "game", "lock_delay");
+ if (lock_delay) {
+ board->game.config.lock_delay = atoi(lock_delay);
+ free(lock_delay);
+ }
+
+ char* force_lock = get_ini_value(&p, "game", "force_lock");
+ if (force_lock) {
+ board->game.config.force_lock = atoi(force_lock);
+ free(force_lock);
+ }
+
+ char* wait_on_clear = get_ini_value(&p, "game", "wait_on_clear");
+ if (wait_on_clear && !strcmp(wait_on_clear, "true")) {
+ board->game.config.wait_on_clear = true;
+ free(wait_on_clear);
+ } else board->game.config.wait_on_clear = false;
+
+ char* line_delay_clear = get_ini_value(&p, "game", "line_clear_delay");
+ if (line_delay_clear) {
+ board->game.config.line_delay_clear = atoi(line_delay_clear);
+ free(line_delay_clear);
+ }
+
+ char *dark = get_ini_value(&p, "ui", "dark_mode");
+ if (dark) {
+ if (!strcmp(dark, "true")) {
+ ui->colors = dark_mode;
+ }
+ free(dark);
+ } else {
+ ui->colors = light_mode;
+ }
+
+ char *skin = get_ini_value(&p, "ui", "skin");
+ if (skin) {
+ ui->config.skin_name = skin;
+ } else ui->config.skin_name = "default";
+ }
+}
+
+void load_solo(cetris_ui *ui) {
+ load_config(ui, &ui->solo.game_board);
+ ui->solo.game_board.scheme = ui->colors;
+
+ ui->solo.main = SDL_CreateTexture(ui->render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 251, 513);
+ ui->solo.queue = SDL_CreateTexture(ui->render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 100, 450);
+ ui->solo.hold = SDL_CreateTexture(ui->render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 100, 100);
+ SDL_SetTextureBlendMode(ui->solo.hold, SDL_BLENDMODE_BLEND);
+
+ ctrs_init_game(&ui->solo.game_board.game);
+ ui->solo.game_board.game.waiting = true;
+ ui->solo.game_board.count_down = 3;
+}
+
+void handle_key(SDL_Event e, cetris_ui *ui, game_board_t* board) {
+ int sym;
+ switch (e.type) {
+ case SDL_QUIT: exit(0);
+ case SDL_KEYDOWN:
+ sym = e.key.keysym.sym;
+ if (sym == ui->config.keys.key_left) {
+ ctrs_move_piece(&board->game, LEFT);
+ } else if (sym == ui->config.keys.key_right) {
+ ctrs_move_piece(&board->game, RIGHT);
+ } else if (sym == ui->config.keys.key_down) {
+ ctrs_move_piece(&board->game, DOWN);
+ } else if (sym == ui->config.keys.key_drop) {
+ ctrs_move_piece(&board->game, HARD_DROP);
+ } else if (sym == ui->config.keys.key_hold) {
+ ctrs_hold_piece(&board->game);
+ } else if (sym == ui->config.keys.key_rotate_cw) {
+ ctrs_move_piece(&board->game, ROTATE_CW);
+ } else if (sym == ui->config.keys.key_rotate_ccw) {
+ ctrs_move_piece(&board->game, ROTATE_CCW);
+ } else if (sym == ui->config.keys.key_restart) {
+ ctrs_init_game(&board->game);
+ board->game.waiting = true;
+ board->count_down = 3;
+ }
+ break;
+ case SDL_KEYUP:
+ sym = e.key.keysym.sym;
+ if (sym == ui->config.keys.key_left) {
+ ctrs_unhold_move(&board->game, LEFT);
+ } else if (sym == ui->config.keys.key_right) {
+ ctrs_unhold_move(&board->game, RIGHT);
+ } else if (sym == ui->config.keys.key_down) {
+ ctrs_unhold_move(&board->game, DOWN);
+ } else if (sym == ui->config.keys.key_drop) {
+ ctrs_unhold_move(&board->game, HARD_DROP);
+ } else if (sym == ui->config.keys.key_rotate_cw) {
+ ctrs_unhold_move(&board->game, ROTATE_CW);
+ } else if (sym == ui->config.keys.key_rotate_ccw) {
+ ctrs_unhold_move(&board->game, ROTATE_CCW);
+ }
+ break;
+ }
+}
+
+void handle_game_events(cetris_ui *ui, game_board_t *board) {
+ if (board->count_down < 0 && board->game.waiting) {
+ ctrs_start_game(&board->game);
+ }
+
+ if (board->game.events.tetris_event > 0) {
+ if (ui->skin.tetris_sound_count > 0) {
+ int index = rand() % ui->skin.tetris_sound_count;
+ SDL_QueueAudio(ui->audio_device, ui->skin.tetris_sound[index].wav_buffer,
+ ui->skin.tetris_sound[index].wav_length);
+ }
+
+ board->game.events.tetris_event--;
+ board->game.events.line_event = 0;
+ board->game.events.lock_event.processed = true;
+ }
+
+ if (board->game.events.line_event > 0) {
+ if (ui->skin.clear_sound_count > 0) {
+ int index;
+ if (ui->skin.random_audio) {
+ index = rand() % ui->skin.clear_sound_count;
+ } else {
+ index = board->game.line_combo - 1;
+ }
+ if (index >= ui->skin.clear_sound_count)
+ index = ui->skin.clear_sound_count - 1;
+
+ SDL_QueueAudio(ui->audio_device, ui->skin.clear_sound[index].wav_buffer,
+ ui->skin.clear_sound[index].wav_length);
+ }
+
+ board->game.events.line_event = 0;
+ board->game.events.lock_event.processed = true;
+ }
+
+ if (!board->game.events.lock_event.processed) {
+ if (ui->skin.lock_sound.wav_buffer) {
+ SDL_QueueAudio(ui->audio_device, ui->skin.lock_sound.wav_buffer,
+ ui->skin.lock_sound.wav_length);
+ }
+
+ board->game.events.lock_event.processed = true;
+ }
+}
+
+int main(void) {
+ cetris_ui ui;
+ memset(&ui, 0, sizeof(cetris_ui));
+
+ setup_sdl(&ui);
+ load_solo(&ui);
+ load_skin(&ui);
+ ui.current_game = SOLO;
+
+ SDL_Event e;
+ for(;;) {
+ while(SDL_PollEvent(&e)) {
+ switch (ui.current_game) {
+ case SOLO:
+ handle_key(e, &ui, &ui.solo.game_board);
+ break;
+ }
+ }
+
+ switch (ui.current_game) {
+ case SOLO:
+ handle_game_events(&ui, &ui.solo.game_board);
+ break;
+ }
+
+ draw(&ui);
+
+ SDL_Delay(1000 / FRAME_RATE);
+ }
+
+ return 0;
+}
diff --git a/frontends/sdl/cetris_sdl.h b/frontends/sdl/cetris_sdl.h
new file mode 100644
index 0000000..26e079e
--- /dev/null
+++ b/frontends/sdl/cetris_sdl.h
@@ -0,0 +1,112 @@
+typedef struct {
+ SDL_Color main;
+ SDL_Color off;
+ SDL_Color text;
+} color_scheme_t;
+
+color_scheme_t dark_mode = {
+ .main = {40, 40, 40, 255},
+ .off = {90, 90, 90, 255},
+ .text = {240, 240, 240, 255}
+};
+
+color_scheme_t light_mode = {
+ .main = {255, 255, 255, 255},
+ .off = {235, 235, 235, 255},
+ .text = {10, 10, 10, 255}
+};
+
+SDL_Color mino_colors[7] = {
+ {253,253,150,255}, // Yellow
+ {174,198,207,255}, // Aqua
+ {255,105,97,255}, // Red
+ {170,221,119,255}, // Olive
+ {255,179,71,255}, // Orange
+ {119,158,203,255}, // Navy
+ {177,156,217,255} // Purple
+};
+
+typedef struct {
+ SDL_AudioSpec wav_spec;
+ uint32_t wav_length;
+ uint8_t *wav_buffer;
+} audio_clip_t;
+
+typedef struct {
+ TTF_Font *font;
+ int size;
+} font_t;
+
+typedef struct {
+ bool random_audio;
+
+ int clear_sound_count;
+ audio_clip_t *clear_sound;
+
+ int tetris_sound_count;
+ audio_clip_t *tetris_sound;
+ audio_clip_t lock_sound;
+
+ SDL_Texture *blocks;
+ SDL_Texture *game_background;
+ SDL_Texture *overlay_top;
+ SDL_Texture *overlay_middle;
+ SDL_Texture *overlay_bottom;
+
+ SDL_Texture *border;
+ SDL_Texture *background;
+
+} cetris_skin_t;
+
+typedef struct {
+ ctrs_game game;
+ color_scheme_t scheme;
+ float count_down;
+} game_board_t;
+
+typedef struct {
+ SDL_Texture *main;
+ SDL_Texture *queue;
+ SDL_Texture *hold;
+
+ game_board_t game_board;
+} solo_game_t;
+
+typedef struct {
+ int key_down;
+ int key_right;
+ int key_left;
+ int key_rotate_cw;
+ int key_rotate_ccw;
+ int key_drop;
+ int key_hold;
+ int key_restart;
+} key_bindings_t;
+
+typedef struct {
+ key_bindings_t keys;
+ char *skin_name;
+} config_t;
+
+enum {
+ SOLO
+};
+
+typedef struct {
+ int current_game;
+ solo_game_t solo;
+ color_scheme_t colors;
+
+ SDL_Renderer* render;
+ SDL_Window *window;
+ SDL_Surface *screen;
+
+ int font_count;
+ font_t fonts[10];
+
+ SDL_AudioDeviceID audio_device;
+
+ config_t config;
+
+ cetris_skin_t skin;
+} cetris_ui;
diff --git a/frontends/sdl/config.ini b/frontends/sdl/config.ini
new file mode 100644
index 0000000..5947b33
--- /dev/null
+++ b/frontends/sdl/config.ini
@@ -0,0 +1,15 @@
+[das]
+das = 220
+arr = 15
+
+[game]
+drop_delay = 20
+next_piece_delay = 40
+lock_delay = 300
+force_lock = 3000
+wait_on_clear = 0
+line_clear_delay = 0
+
+[ui]
+skin = puyo_arles_skin
+dark_mode = true
diff --git a/frontends/sdl/data.zip b/frontends/sdl/data.zip
new file mode 100644
index 0000000..a26e00b
--- /dev/null
+++ b/frontends/sdl/data.zip
Binary files differ
diff --git a/frontends/sdl/meson.build b/frontends/sdl/meson.build
new file mode 100644
index 0000000..5760408
--- /dev/null
+++ b/frontends/sdl/meson.build
@@ -0,0 +1,14 @@
+src = ['cetris_sdl.c']
+
+inc = [cetris_inc]
+deps = []
+
+deps += compiler.find_library('m')
+deps += dependency('SDL2')
+deps += dependency('SDL2_ttf')
+deps += dependency('SDL2_image')
+
+executable('cetris', src,
+ dependencies: deps,
+ include_directories: inc,
+ install: false)
diff --git a/frontends/timer.c b/frontends/timer.c
new file mode 100644
index 0000000..9ae90ca
--- /dev/null
+++ b/frontends/timer.c
@@ -0,0 +1,86 @@
+#ifdef _WIN32
+#include <windows.h>
+
+#if CETRIS_HI_RES
+static DWORD WINAPI ctrs_game_loop(void* data) {
+ ctrs_game* game = (ctrs_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 (!ctrs_update_game_tick(game)) {
+ break;
+ }
+ Sleep(1);
+ }
+ return 0;
+}
+#else
+static DWORD WINAPI ctrs_game_loop(void* data) {
+ ctrs_game* game = (ctrs_game*)data;
+ while(1) {
+ if (game->waiting) break;
+ game->tick += 16;
+ if (!ctrs_update_game_tick(game)) {
+ break;
+ }
+ Sleep(16); // little less than 60hz
+ }
+}
+#endif
+static void ctrs_start_game(ctrs_game *g) {
+ g->waiting = false;
+ HANDLE thread = CreateThread(NULL, 0, ctrs_game_loop, g, 0, NULL);
+}
+#else
+#include <pthread.h>
+#include <unistd.h>
+
+#if CETRIS_HI_RES
+#include <time.h>
+static void *ctrs_game_loop(void* data) {
+ ctrs_game *game = (ctrs_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 (!ctrs_update_game_tick(game)) {
+ break;
+ }
+ usleep(1000);
+ }
+ return 0;
+}
+#else
+static void *ctrs_game_loop(void *data) {
+ ctrs_game* game = (ctrs_game*)data;
+ while(1) {
+ if (game->waiting) break;
+ game->tick += 16;
+ if (!ctrs_update_game_tick(game)) {
+ break;
+ }
+ // could be more accurate, keeping
+ // it consistant with windows
+ usleep(16000);
+ }
+}
+#endif
+static void ctrs_start_game(ctrs_game *g) {
+ g->waiting = false;
+ pthread_t thread;
+ pthread_create(&thread, NULL, ctrs_game_loop, (void*)g);
+}
+#endif