551 lines
18 KiB
C
551 lines
18 KiB
C
#include "world.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* Tetris game dimensions */
|
|
#define BOARD_WIDTH 10
|
|
#define BOARD_HEIGHT 20
|
|
#define CELL_WIDTH 2 /* Double-width cells for square appearance */
|
|
#define BOARD_RENDER_X 2 /* Left panel position */
|
|
#define BOARD_RENDER_Y 2
|
|
#define STATS_X 26 /* Right panel position */
|
|
#define STATS_Y 2
|
|
|
|
/* Tetromino definitions - 7 standard tetrominoes */
|
|
enum PieceType {
|
|
PIECE_I, PIECE_O, PIECE_T, PIECE_S, PIECE_Z, PIECE_J, PIECE_L, NUM_PIECES
|
|
};
|
|
|
|
/* Piece colors and characters - use different characters for different piece types */
|
|
static const int PIECE_COLORS[NUM_PIECES] = {
|
|
COLOR_CYAN, /* I */
|
|
COLOR_YELLOW, /* O */
|
|
COLOR_MAGENTA, /* T */
|
|
COLOR_GREEN, /* S */
|
|
COLOR_RED, /* Z */
|
|
COLOR_BLUE, /* J */
|
|
COLOR_WHITE /* L */
|
|
};
|
|
|
|
static const char PIECE_CHARS[NUM_PIECES] = {
|
|
'#', /* I */
|
|
'O', /* O */
|
|
'T', /* T */
|
|
'S', /* S */
|
|
'Z', /* Z */
|
|
'J', /* J */
|
|
'L' /* L */
|
|
};
|
|
|
|
/* Rotation states for each tetromino (4 rotations, 4 blocks each) */
|
|
/* Format: (x, y) offset from piece center */
|
|
static const int PIECE_SHAPES[NUM_PIECES][4][4][2] = {
|
|
/* I piece */
|
|
{
|
|
{{-1, 0}, {0, 0}, {1, 0}, {2, 0}}, /* Rotation 0 */
|
|
{{0, -1}, {0, 0}, {0, 1}, {0, 2}}, /* Rotation 1 */
|
|
{{-1, 0}, {0, 0}, {1, 0}, {2, 0}}, /* Rotation 2 */
|
|
{{0, -1}, {0, 0}, {0, 1}, {0, 2}} /* Rotation 3 */
|
|
},
|
|
/* O piece (square - no rotation needed) */
|
|
{
|
|
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
|
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
|
{{0, 0}, {1, 0}, {0, 1}, {1, 1}},
|
|
{{0, 0}, {1, 0}, {0, 1}, {1, 1}}
|
|
},
|
|
/* T piece */
|
|
{
|
|
{{-1, 0}, {0, 0}, {1, 0}, {0, 1}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {1, 0}},
|
|
{{-1, 0}, {0, 0}, {1, 0}, {0, -1}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {-1, 0}}
|
|
},
|
|
/* S piece */
|
|
{
|
|
{{0, 0}, {1, 0}, {-1, 1}, {0, 1}},
|
|
{{0, -1}, {0, 0}, {1, 0}, {1, 1}},
|
|
{{0, 0}, {1, 0}, {-1, 1}, {0, 1}},
|
|
{{0, -1}, {0, 0}, {1, 0}, {1, 1}}
|
|
},
|
|
/* Z piece */
|
|
{
|
|
{{-1, 0}, {0, 0}, {0, 1}, {1, 1}},
|
|
{{1, -1}, {0, 0}, {1, 0}, {0, 1}},
|
|
{{-1, 0}, {0, 0}, {0, 1}, {1, 1}},
|
|
{{1, -1}, {0, 0}, {1, 0}, {0, 1}}
|
|
},
|
|
/* J piece */
|
|
{
|
|
{{-1, 0}, {0, 0}, {1, 0}, {1, 1}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {1, -1}},
|
|
{{-1, -1}, {-1, 0}, {0, 0}, {1, 0}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {-1, 1}}
|
|
},
|
|
/* L piece */
|
|
{
|
|
{{-1, 0}, {0, 0}, {1, 0}, {-1, 1}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {1, 1}},
|
|
{{-1, -1}, {-1, 0}, {0, 0}, {1, 0}},
|
|
{{0, -1}, {0, 0}, {0, 1}, {-1, -1}}
|
|
}
|
|
};
|
|
|
|
/* Game state structure */
|
|
typedef struct {
|
|
/* Board: 1 = filled, 0 = empty */
|
|
int board[BOARD_HEIGHT][BOARD_WIDTH];
|
|
|
|
/* Current piece state */
|
|
int piece_type;
|
|
int piece_x, piece_y;
|
|
int piece_rotation;
|
|
|
|
/* Next piece preview */
|
|
int next_piece_type;
|
|
|
|
/* Game statistics */
|
|
int score;
|
|
int level;
|
|
int lines_cleared;
|
|
int total_lines;
|
|
|
|
/* Game state */
|
|
int game_over;
|
|
int paused;
|
|
|
|
/* Timing */
|
|
int gravity_counter;
|
|
int gravity_speed;
|
|
int frames_since_last_gravity;
|
|
|
|
/* Display dimensions */
|
|
int screen_width;
|
|
int screen_height;
|
|
} GameState;
|
|
|
|
/* Initialize game state */
|
|
void* init_game() {
|
|
GameState* state = malloc(sizeof(GameState));
|
|
memset(state, 0, sizeof(GameState));
|
|
|
|
/* Initialize board */
|
|
for (int y = 0; y < BOARD_HEIGHT; y++) {
|
|
for (int x = 0; x < BOARD_WIDTH; x++) {
|
|
state->board[y][x] = 0;
|
|
}
|
|
}
|
|
|
|
state->level = 1;
|
|
state->gravity_speed = 30 / state->level; /* Exponential difficulty - much harder per level */
|
|
if (state->gravity_speed < 1) state->gravity_speed = 1;
|
|
state->next_piece_type = rand() % NUM_PIECES;
|
|
|
|
return state;
|
|
}
|
|
|
|
/* Destroy game state */
|
|
void destroy_game(void* game) {
|
|
free(game);
|
|
}
|
|
|
|
/* Check if a piece can be placed at given position and rotation */
|
|
int can_place_piece(GameState* state, int type, int x, int y, int rotation) {
|
|
const int (*shape)[4][2] = &PIECE_SHAPES[type][rotation];
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
int bx = x + (*shape)[i][0];
|
|
int by = y + (*shape)[i][1];
|
|
|
|
/* Check bounds */
|
|
if (bx < 0 || bx >= BOARD_WIDTH || by < 0 || by >= BOARD_HEIGHT) {
|
|
return 0;
|
|
}
|
|
|
|
/* Check collision */
|
|
if (state->board[by][bx] != 0) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Spawn a new piece */
|
|
void spawn_piece(GameState* state) {
|
|
state->piece_type = state->next_piece_type;
|
|
state->next_piece_type = rand() % NUM_PIECES;
|
|
state->piece_x = BOARD_WIDTH / 2;
|
|
state->piece_y = 0;
|
|
state->piece_rotation = 0;
|
|
|
|
/* Check if game is over (piece can't spawn) */
|
|
if (!can_place_piece(state, state->piece_type, state->piece_x, state->piece_y, 0)) {
|
|
state->game_over = 1;
|
|
}
|
|
}
|
|
|
|
/* Move piece left */
|
|
void move_piece_left(GameState* state) {
|
|
if (can_place_piece(state, state->piece_type, state->piece_x - 1, state->piece_y, state->piece_rotation)) {
|
|
state->piece_x--;
|
|
}
|
|
}
|
|
|
|
/* Move piece right */
|
|
void move_piece_right(GameState* state) {
|
|
if (can_place_piece(state, state->piece_type, state->piece_x + 1, state->piece_y, state->piece_rotation)) {
|
|
state->piece_x++;
|
|
}
|
|
}
|
|
|
|
/* Soft drop (move piece down) */
|
|
int move_piece_down(GameState* state) {
|
|
if (can_place_piece(state, state->piece_type, state->piece_x, state->piece_y + 1, state->piece_rotation)) {
|
|
state->piece_y++;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Hard drop (instant drop) */
|
|
void hard_drop_piece(GameState* state) {
|
|
while (move_piece_down(state));
|
|
}
|
|
|
|
/* Rotate piece clockwise */
|
|
void rotate_piece(GameState* state) {
|
|
int new_rotation = (state->piece_rotation + 1) % 4;
|
|
if (can_place_piece(state, state->piece_type, state->piece_x, state->piece_y, new_rotation)) {
|
|
state->piece_rotation = new_rotation;
|
|
}
|
|
}
|
|
|
|
/* Lock piece into board and spawn next piece */
|
|
void lock_piece(GameState* state) {
|
|
const int (*shape)[4][2] = &PIECE_SHAPES[state->piece_type][state->piece_rotation];
|
|
|
|
/* Place piece blocks on board */
|
|
for (int i = 0; i < 4; i++) {
|
|
int bx = state->piece_x + (*shape)[i][0];
|
|
int by = state->piece_y + (*shape)[i][1];
|
|
|
|
if (bx >= 0 && bx < BOARD_WIDTH && by >= 0 && by < BOARD_HEIGHT) {
|
|
state->board[by][bx] = state->piece_type + 1; /* +1 so 0 means empty */
|
|
}
|
|
}
|
|
|
|
spawn_piece(state);
|
|
}
|
|
|
|
/* Check and clear completed lines */
|
|
void check_and_clear_lines(GameState* state) {
|
|
int lines_to_clear = 0;
|
|
int new_board[BOARD_HEIGHT][BOARD_WIDTH];
|
|
|
|
/* Identify complete lines */
|
|
int write_row = BOARD_HEIGHT - 1;
|
|
|
|
for (int y = BOARD_HEIGHT - 1; y >= 0; y--) {
|
|
int is_complete = 1;
|
|
|
|
for (int x = 0; x < BOARD_WIDTH; x++) {
|
|
if (state->board[y][x] == 0) {
|
|
is_complete = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!is_complete) {
|
|
memcpy(new_board[write_row], state->board[y], sizeof(state->board[y]));
|
|
write_row--;
|
|
} else {
|
|
lines_to_clear++;
|
|
}
|
|
}
|
|
|
|
/* Clear top rows */
|
|
for (int y = 0; y <= write_row; y++) {
|
|
memset(new_board[y], 0, sizeof(new_board[y]));
|
|
}
|
|
|
|
if (lines_to_clear > 0) {
|
|
memcpy(state->board, new_board, sizeof(state->board));
|
|
state->total_lines += lines_to_clear;
|
|
state->lines_cleared = lines_to_clear;
|
|
|
|
/* Update score: higher points for clearing multiple lines */
|
|
int points = 0;
|
|
switch (lines_to_clear) {
|
|
case 1: points = 100; break;
|
|
case 2: points = 300; break;
|
|
case 3: points = 500; break;
|
|
case 4: points = 800; break;
|
|
}
|
|
state->score += points * state->level;
|
|
|
|
/* Update level every 10 lines */
|
|
int new_level = state->total_lines / 10 + 1;
|
|
if (new_level != state->level) {
|
|
state->level = new_level;
|
|
state->gravity_speed = 30 / state->level;
|
|
if (state->gravity_speed < 1) state->gravity_speed = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Apply gravity tick - move piece down automatically */
|
|
void apply_gravity(GameState* state) {
|
|
state->frames_since_last_gravity++;
|
|
|
|
if (state->frames_since_last_gravity >= state->gravity_speed) {
|
|
state->frames_since_last_gravity = 0;
|
|
|
|
if (!move_piece_down(state)) {
|
|
/* Piece can't move down, lock it */
|
|
lock_piece(state);
|
|
check_and_clear_lines(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Render the game */
|
|
void render_game(GameState* state) {
|
|
/* Clear screen */
|
|
clear_screen();
|
|
|
|
/* Draw board border (left panel) */
|
|
int board_width = BOARD_WIDTH * CELL_WIDTH;
|
|
int board_height = BOARD_HEIGHT;
|
|
int board_x = BOARD_RENDER_X;
|
|
int board_y = BOARD_RENDER_Y;
|
|
|
|
/* Top border */
|
|
set_color_cell(0x250C, board_x, board_y, COLOR_WHITE, COLOR_BLACK); /* ┌ */
|
|
for (int x = 1; x < board_width + 1; x++) {
|
|
set_color_cell(0x2500, board_x + x, board_y, COLOR_WHITE, COLOR_BLACK); /* ─ */
|
|
}
|
|
set_color_cell(0x2510, board_x + board_width + 1, board_y, COLOR_WHITE, COLOR_BLACK); /* ┐ */
|
|
|
|
/* Left border */
|
|
for (int y = 1; y <= board_height; y++) {
|
|
set_color_cell(0x2502, board_x, board_y + y, COLOR_WHITE, COLOR_BLACK); /* │ */
|
|
}
|
|
|
|
/* Right border */
|
|
for (int y = 1; y <= board_height; y++) {
|
|
set_color_cell(0x2502, board_x + board_width + 1, board_y + y, COLOR_WHITE, COLOR_BLACK); /* │ */
|
|
}
|
|
|
|
/* Bottom border */
|
|
set_color_cell(0x2514, board_x, board_y + board_height + 1, COLOR_WHITE, COLOR_BLACK); /* └ */
|
|
for (int x = 1; x < board_width + 1; x++) {
|
|
set_color_cell(0x2500, board_x + x, board_y + board_height + 1, COLOR_WHITE, COLOR_BLACK); /* ─ */
|
|
}
|
|
set_color_cell(0x2518, board_x + board_width + 1, board_y + board_height + 1, COLOR_WHITE, COLOR_BLACK); /* ┘ */
|
|
|
|
/* Draw board cells */
|
|
for (int y = 0; y < BOARD_HEIGHT; y++) {
|
|
for (int x = 0; x < BOARD_WIDTH; x++) {
|
|
int screen_x = board_x + 1 + x * CELL_WIDTH;
|
|
int screen_y = board_y + 1 + y;
|
|
|
|
if (state->board[y][x] != 0) {
|
|
int piece_type = state->board[y][x] - 1;
|
|
/* Ensure piece_type is valid */
|
|
if (piece_type < 0 || piece_type >= NUM_PIECES) {
|
|
piece_type = 0; /* Default to I piece if invalid */
|
|
}
|
|
set_color_cell(PIECE_CHARS[piece_type], screen_x, screen_y, PIECE_COLORS[piece_type], COLOR_BLACK);
|
|
set_color_cell(PIECE_CHARS[piece_type], screen_x + 1, screen_y, PIECE_COLORS[piece_type], COLOR_BLACK);
|
|
} else {
|
|
set_color_cell(' ', screen_x, screen_y, COLOR_WHITE, COLOR_BLACK);
|
|
set_color_cell(' ', screen_x + 1, screen_y, COLOR_WHITE, COLOR_BLACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Draw current piece (ghost preview at landing position) */
|
|
const int (*shape)[4][2] = &PIECE_SHAPES[state->piece_type][state->piece_rotation];
|
|
|
|
/* Draw ghost piece */
|
|
int ghost_y = state->piece_y;
|
|
while (can_place_piece(state, state->piece_type, state->piece_x, ghost_y + 1, state->piece_rotation)) {
|
|
ghost_y++;
|
|
}
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
int bx = state->piece_x + (*shape)[i][0];
|
|
int by = ghost_y + (*shape)[i][1];
|
|
|
|
if (bx >= 0 && bx < BOARD_WIDTH && by >= 0 && by < BOARD_HEIGHT) {
|
|
int screen_x = board_x + 1 + bx * CELL_WIDTH;
|
|
int screen_y = board_y + 1 + by;
|
|
set_color_cell('.', screen_x, screen_y, COLOR_GREEN, COLOR_BLACK);
|
|
set_color_cell('.', screen_x + 1, screen_y, COLOR_GREEN, COLOR_BLACK);
|
|
}
|
|
}
|
|
|
|
/* Draw current piece */
|
|
for (int i = 0; i < 4; i++) {
|
|
int bx = state->piece_x + (*shape)[i][0];
|
|
int by = state->piece_y + (*shape)[i][1];
|
|
|
|
if (bx >= 0 && bx < BOARD_WIDTH && by >= 0 && by < BOARD_HEIGHT) {
|
|
int screen_x = board_x + 1 + bx * CELL_WIDTH;
|
|
int screen_y = board_y + 1 + by;
|
|
set_color_cell(PIECE_CHARS[state->piece_type], screen_x, screen_y, PIECE_COLORS[state->piece_type], COLOR_BLACK);
|
|
set_color_cell(PIECE_CHARS[state->piece_type], screen_x + 1, screen_y, PIECE_COLORS[state->piece_type], COLOR_BLACK);
|
|
}
|
|
}
|
|
|
|
/* Draw stats panel (right side) */
|
|
int stats_x = STATS_X;
|
|
int stats_y = STATS_Y;
|
|
|
|
/* Stats border */
|
|
set_color_cell(0x250C, stats_x, stats_y, COLOR_CYAN, COLOR_BLACK); /* ┌ */
|
|
for (int x = 1; x < 18; x++) {
|
|
set_color_cell(0x2500, stats_x + x, stats_y, COLOR_CYAN, COLOR_BLACK); /* ─ */
|
|
}
|
|
set_color_cell(0x2510, stats_x + 18, stats_y, COLOR_CYAN, COLOR_BLACK); /* ┐ */
|
|
|
|
for (int y = 1; y <= 8; y++) {
|
|
set_color_cell(0x2502, stats_x, stats_y + y, COLOR_CYAN, COLOR_BLACK); /* │ */
|
|
set_color_cell(0x2502, stats_x + 18, stats_y + y, COLOR_CYAN, COLOR_BLACK);
|
|
}
|
|
|
|
set_color_cell(0x2514, stats_x, stats_y + 9, COLOR_CYAN, COLOR_BLACK); /* └ */
|
|
for (int x = 1; x < 18; x++) {
|
|
set_color_cell(0x2500, stats_x + x, stats_y + 9, COLOR_CYAN, COLOR_BLACK); /* ─ */
|
|
}
|
|
set_color_cell(0x2518, stats_x + 18, stats_y + 9, COLOR_CYAN, COLOR_BLACK); /* ┘ */
|
|
|
|
/* Draw stats text */
|
|
char buffer[32];
|
|
snprintf(buffer, sizeof(buffer), "SCORE:%d", state->score);
|
|
set_message(buffer, stats_x + 2, stats_y + 1);
|
|
|
|
snprintf(buffer, sizeof(buffer), "LEVEL:%d", state->level);
|
|
set_message(buffer, stats_x + 2, stats_y + 2);
|
|
|
|
snprintf(buffer, sizeof(buffer), "LINES:%d", state->total_lines);
|
|
set_message(buffer, stats_x + 2, stats_y + 3);
|
|
|
|
snprintf(buffer, sizeof(buffer), "NEXT:");
|
|
set_message(buffer, stats_x + 2, stats_y + 5);
|
|
|
|
/* Draw next piece preview */
|
|
const int (*next_shape)[4][2] = &PIECE_SHAPES[state->next_piece_type][0];
|
|
for (int i = 0; i < 4; i++) {
|
|
int dx = next_shape[0][i][0];
|
|
int dy = next_shape[0][i][1];
|
|
int px = stats_x + 3 + dx;
|
|
int py = stats_y + 6 + dy;
|
|
|
|
if (px > stats_x && px < stats_x + 18 && py > stats_y && py < stats_y + 9) {
|
|
set_color_cell(PIECE_CHARS[state->next_piece_type], px, py, PIECE_COLORS[state->next_piece_type], COLOR_BLACK);
|
|
}
|
|
}
|
|
|
|
/* Draw status bar at bottom */
|
|
int status_y = 23;
|
|
char status_msg[80];
|
|
|
|
if (state->game_over) {
|
|
snprintf(status_msg, sizeof(status_msg), "GAME OVER - Press 'q' to quit or 'r' to restart");
|
|
} else if (state->paused) {
|
|
snprintf(status_msg, sizeof(status_msg), "PAUSED - Press 'p' to resume or 'q' to quit");
|
|
} else {
|
|
snprintf(status_msg, sizeof(status_msg), "Arrows: move | Z: rotate | Space: drop | P: pause | Q: quit");
|
|
}
|
|
|
|
set_message(status_msg, 0, status_y);
|
|
}
|
|
|
|
/* Game event handler */
|
|
int world_event(struct event* event, void* game) {
|
|
GameState* state = (GameState*)game;
|
|
state->screen_width = event->width;
|
|
state->screen_height = event->height;
|
|
|
|
if (event->type == EVENT_START) {
|
|
spawn_piece(state);
|
|
} else if (event->type == EVENT_KEY) {
|
|
if (!state->game_over && !state->paused) {
|
|
switch (event->key) {
|
|
case KEY_LEFT:
|
|
move_piece_left(state);
|
|
break;
|
|
case KEY_RIGHT:
|
|
move_piece_right(state);
|
|
break;
|
|
case KEY_DOWN:
|
|
move_piece_down(state);
|
|
break;
|
|
case ' ': /* Hard drop */
|
|
hard_drop_piece(state);
|
|
lock_piece(state);
|
|
check_and_clear_lines(state);
|
|
break;
|
|
case 'z':
|
|
case 'Z':
|
|
rotate_piece(state);
|
|
break;
|
|
case 'p':
|
|
case 'P':
|
|
state->paused = 1;
|
|
break;
|
|
case 'q':
|
|
case 'Q':
|
|
destroy_game(state);
|
|
return 1; /* Exit game */
|
|
}
|
|
} else if (state->paused) {
|
|
switch (event->key) {
|
|
case 'p':
|
|
case 'P':
|
|
state->paused = 0;
|
|
break;
|
|
case 'q':
|
|
case 'Q':
|
|
destroy_game(state);
|
|
return 1; /* Exit game */
|
|
}
|
|
} else if (state->game_over) {
|
|
switch (event->key) {
|
|
case 'r':
|
|
case 'R':
|
|
/* Restart game */
|
|
memset(state->board, 0, sizeof(state->board));
|
|
state->score = 0;
|
|
state->level = 1;
|
|
state->lines_cleared = 0;
|
|
state->total_lines = 0;
|
|
state->game_over = 0;
|
|
state->gravity_speed = 30 / state->level;
|
|
if (state->gravity_speed < 1) state->gravity_speed = 1;
|
|
spawn_piece(state);
|
|
break;
|
|
case 'q':
|
|
case 'Q':
|
|
destroy_game(state);
|
|
return 1; /* Exit game */
|
|
}
|
|
}
|
|
} else if (event->type == EVENT_TIMEOUT) {
|
|
if (!state->paused && !state->game_over) {
|
|
apply_gravity(state);
|
|
}
|
|
}
|
|
|
|
render_game(state);
|
|
return 0; /* Continue game */
|
|
}
|
|
|
|
int main() {
|
|
game_speed(50); /* 50ms per frame = 20 FPS for faster gameplay */
|
|
return start_world(init_game, world_event, destroy_game);
|
|
}
|