njetris/tetris.c

612 lines
18 KiB
C

// Tetris in C using raylib
#include "tetris.h"
// Game state
unsigned char board[BOARD_HEIGHT][BOARD_WIDTH] = {0};
unsigned int iBoardMask[BOARD_HEIGHT] = {0}; // bitmask for each row
unsigned int iPieceMask[BOARD_HEIGHT + PIECE_BUFFER_OFFSET] = {0}; // bitmask for piece position
bool board_mask[BOARD_HEIGHT][BOARD_WIDTH] = {0};
bool piece_mask[BOARD_HEIGHT+PIECE_BUFFER_OFFSET][BOARD_WIDTH] = {0};
char piece_rotation = 0;
// Piece shapes[] = { T_PIECE, O_PIECE, Z_PIECE, S_PIECE, I_PIECE, J_PIECE, L_PIECE};
// Main function
int main(){
printf("Let's play some tetris motherfucker");
InitWindow(BOARD_WIDTH*GRID_SCREEN_PIXELS, BOARD_HEIGHT*GRID_SCREEN_PIXELS, "yaa");
SetTargetFPS(60);
zero_board();
init_game();
while(1){
BeginDrawing();
update();
draw();
EndDrawing();
if(WindowShouldClose()) break;
}
cleanup_shapes();
CloseWindow();
return 0;
}
// Initialize shapes array properly
Piece shapes[7];
bool shapes_initialized = false;
Piece* current_piece = NULL;
void init_shapes() {
if(shapes_initialized) return;
shapes[0] = T_PIECE;
shapes[1] = O_PIECE;
shapes[2] = Z_PIECE;
shapes[3] = S_PIECE;
shapes[4] = I_PIECE;
shapes[5] = J_PIECE;
shapes[6] = L_PIECE;
shapes_initialized = true;
}
void cleanup_shapes() {
for(int i = 0; i < 7; i++) {
free_shape(shapes[i].shape, shapes[i].dims.y);
}
}
void init_game(){
init_shapes();
zero_board();
zero_board_mask();
zero_piece_mask();
spawn_piece();
}
// Function implementations
void zero_board(){
for(int i = 0; i < BOARD_HEIGHT; i++){
for(int j = 0; j < BOARD_WIDTH; j++){
board[i][j] = 0;
}
}
}
void zero_board_mask(){
for(int iter_height = 0; iter_height < BOARD_HEIGHT; iter_height++){
for(int iter_width = 0; iter_width < BOARD_WIDTH; iter_width++){
board_mask[iter_height][iter_width] = false;
}
}
}
void zero_piece_mask(){
for(int iter_height = 0; iter_height < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; iter_height++){
for(int iter_width = 0; iter_width < BOARD_WIDTH; iter_width++){
piece_mask[iter_height][iter_width] = false;
}
}
}
void bool_array_fill(bool* target, int len, bool val){
for(int i = 0; i < len; i++){
target[i] = val;
}
}
void get_filled_lines(bool* target){
bool_array_fill(target, BOARD_HEIGHT, 0);
bool filled_line_array[BOARD_WIDTH] = {[0 ... BOARD_WIDTH-1] = 1};
// loop over board lines
for(int iter_y = 0; iter_y < BOARD_HEIGHT; iter_y++){
if(memcmp(board_mask[iter_y], filled_line_array, sizeof(bool)*BOARD_WIDTH) == 0){
target[iter_y] = 1;
continue;
}
}
}
void clear_lines(){
bool filled_lines[BOARD_HEIGHT] = {0};
get_filled_lines(filled_lines);
int offset_lines = 0; // count the offset we need to keep when checking the rest of the lines
for(int line = BOARD_HEIGHT - 1; line > 0; line--){
int hitline = line+offset_lines;
if(!filled_lines[line]) // The line we are iterating over is not filled
{
continue;
}
// shift all lines down one starting at hitline
for(int shift_line = hitline; shift_line > 0; shift_line--){
for(int iter_x = 0; iter_x < BOARD_WIDTH; iter_x++){
board[shift_line][iter_x] = board[shift_line-1][iter_x];
board_mask[shift_line][iter_x] = board_mask[shift_line-1][iter_x];
}
}
// All the lines are now shifted down (moved to higher index) so we need to start shifting offset by +1 now.
offset_lines++;
}
}
void populate_board_mask(){
for(int i = 0; i < BOARD_HEIGHT; i++){
for(int j = 0; j < BOARD_WIDTH; j++){
if(board[i][j] != 0){
board_mask[i][j] = true;
} else {
board_mask[i][j] = false;
}
}
}
}
bool next_tick_valid(){
populate_board_mask();
for(int iter_height = 0; iter_height < BOARD_HEIGHT; iter_height++)
{
for(int iter_width = 0; iter_width < BOARD_WIDTH; iter_width++)
{
if( board_mask[iter_height][iter_width] &&
piece_mask[iter_height-1+PIECE_BUFFER_OFFSET][iter_width] )
{
return false;
}
}
}
return true;
}
bool piece_tick(){
if(!next_tick_valid()){
return 0;
}
// Check if piece can go down
// We should convert this to a simpler check where the booleans are converted to a single binary value and compared
for(int k = 0; k < BOARD_WIDTH; k++){
if(piece_mask[BOARD_HEIGHT-1 + PIECE_BUFFER_OFFSET][k]){
return 0; // piece would go out of bounds
}
}
// Move piece down
for(int j = BOARD_HEIGHT - 1 + PIECE_BUFFER_OFFSET; j >= 0; j--){
for(int i = 0; i < BOARD_WIDTH; i++){
if(piece_mask[j][i]){
piece_mask[j+1][i] = piece_mask[j][i]; // This somehow doesn't crash out of bounds?
piece_mask[j][i] = 0;
}
}
}
return 1;
}
void place_piece(){
for(int i = 0; i < BOARD_HEIGHT+PIECE_BUFFER_OFFSET; i++){
for(int j = 0; j < BOARD_WIDTH; j++){
if(piece_mask[i][j]){
board[i-PIECE_BUFFER_OFFSET][j] = current_piece->color;
}
}
}
populate_board_mask();
}
void spawn_piece(){
current_piece = &shapes[GetRandomValue(0, 6)];
piece_rotation = 0;
// position piece at top of board
// calculate piece_mask based on shape and position
Vector2* bounds = get_shape_bounds();
int spawn_x = (BOARD_WIDTH / 2) - (int)bounds[0].x; // center horizontally, adjust for shape bounds
int spawn_y = 2 + (int)bounds[0].y;
for(int i = 0; i < current_piece->dims.y; i++){
for(int j = 0; j < current_piece->dims.x; j++){
if(current_piece->shape[i][j] == 1){
int board_x = spawn_x + j;
int board_y = spawn_y + i;
if(board_x >= 0 && board_x < BOARD_WIDTH && board_y >= 0 && board_y < BOARD_HEIGHT){
piece_mask[board_y][board_x] = true;
}
}
}
}
}
// void Vector2Rotate(Vector2* v, float angle){
// if (v == NULL) return;
// float rad = angle * (3.14159265 / 180.0);
// float cosA = cos(rad);
// float sinA = sin(rad);
// float x_new = v->x * cosA - v->y * sinA;
// float y_new = v->x * sinA + v->y * cosA;
// v->x = x_new;
// v->y = y_new;
// }
void Vector2Rotate(Vector2* v, float angle){
if (v == NULL) return;
float rad = angle * (PI / 180.0);
float cosA = cosf(rad);
float sinA = sinf(rad);
// Store original values
float orig_x = v->x;
float orig_y = v->y;
// Calculate new values using original coordinates
v->x = orig_x * cosA - orig_y * sinA;
v->y = orig_x * sinA + orig_y * cosA;
}
Vector2 Vector2Subtract(Vector2* v1, Vector2* v2){
Vector2 result = (Vector2) {0,0};
result.x = v1->x - v2->x;
result.y = v1->y - v2->y;
return result;
}
void debug_draw_blocks_on_axis(Vector2 pblock_vecs[4], Color color){
bool debugDrawing = 1;
int numVecs = sizeof(pblock_vecs) / sizeof(pblock_vecs[0]);
Vector2 block_vecs[numVecs];
#ifdef NDEBUG
int axis_zero_x = BOARD_WIDTH / 2;
int axis_zero_y = BOARD_HEIGHT / 2;
BeginDrawing();
//
// Draw axis
for(int i = -5; i <= 5; i++){
debugDraw(axis_zero_x + i, axis_zero_y, (Color){255,255,0,255});
debugDraw(axis_zero_x, axis_zero_y + i, (Color){255,255,0,255});
}
for(int i = 0; i < numVecs; i++){
debugDraw((int)(block_vecs[i].x)+axis_zero_x, (int)(block_vecs[i].y+axis_zero_y), (Color) color);
}
asm("nop"); // debug breakpoint
draw(); // reset
EndDrawing();
#endif
}
void debug_draw_blocks(Vector2 block_vecs[1], Color color){
bool debugDrawing = 1;
int numVecs = sizeof(block_vecs) / sizeof(block_vecs[0]);
#ifdef NDEBUG
BeginDrawing();
for(int i = 0; i < numVecs; i++){
debugDraw((int)(block_vecs[i].x), (int)(block_vecs[i].y), (Color) color);
}
asm("nop"); // debug breakpoint
draw(); // reset
EndDrawing();
#endif
}
void turn_piece(bool clockwise){
if(!current_piece || !current_piece->has_origin){
return;
}
Piece* cpiece = current_piece; // Assuming current_piece is already Piece*
Vector2 block_vecs[4] = {{0,0},{0,0},{0,0},{0,0}};
// Verified fixed
get_vecs_from_shape(cpiece, block_vecs);
asm("nop"); // debug breakpoint
// rotate blocks from shape data to match currently tracked orientation
// somehow this turns a J piece into a T piece. wtf
// Apply current rotation incrementally to avoid precision errors
for(int rotation_step = 0; rotation_step < piece_rotation; rotation_step++){
for(int i = 0; i < 4; i++){
Vector2Rotate(&block_vecs[i], 90); // Always rotate by 90°
block_vecs[i].x = roundf(block_vecs[i].x); // Round instead of truncate
block_vecs[i].y = roundf(block_vecs[i].y);
}
}
debug_draw_blocks_on_axis(block_vecs, (Color){255,255,255,255});
// sort blocks, also broken
sort_blocks(block_vecs);
debug_draw_blocks_on_axis(block_vecs, (Color){255,255,255,255});
asm("nop"); // debug breakpoint
Vector2 topleftmost_block = find_topleftmost_block_from_buffer();
Vector2 origin_on_piece_mask = Vector2Subtract(&topleftmost_block,&block_vecs[0]);
debug_draw_blocks((Vector2[1]){(Vector2){(int)origin_on_piece_mask.x, (int)origin_on_piece_mask.y-PIECE_BUFFER_OFFSET}}, (Color){255,255,255,255});
// clear piece mask
// try to rotate to new position now
Vector2 test_vecs[4];
memcpy(test_vecs, block_vecs, sizeof(block_vecs));
for(int i = 0; i < 4; i++){
Vector2Rotate(&test_vecs[i], 90 * (clockwise ? 1 : -1));
int new_y = (float)(int)(test_vecs[i].y + origin_on_piece_mask.y);
int new_x = (float)(int)(test_vecs[i].x + origin_on_piece_mask.x);
if(new_x < 0 || new_x >= BOARD_WIDTH){
return; // out of bounds
}
if(new_y < 0 || new_y >= BOARD_HEIGHT + PIECE_BUFFER_OFFSET){
return; // out of bounds
}
if(board_mask[new_y - PIECE_BUFFER_OFFSET][new_x]){
return; // collision
}
test_vecs[i].x = roundf(test_vecs[i].x);
test_vecs[i].y = roundf(test_vecs[i].y);
}
zero_piece_mask();
for(int i = 0; i < 4; i++){
Vector2 vec = test_vecs[i];
piece_mask[(int)(vec.y + origin_on_piece_mask.y)][(int)(vec.x + origin_on_piece_mask.x)] = true;
}
// update piece rotation tracker
piece_rotation += clockwise ? 1 : -1;
if(piece_rotation > 3) piece_rotation = 0;
if(piece_rotation < 0) piece_rotation = 3;
}
Vector2* get_shape_bounds(){
static Vector2 bounds[2]; // [0] = origin--> top-left, [1] = bottom-right
Piece* cpiece = current_piece;
Vector2* origin = &cpiece->origin;
Vector2* dims = &cpiece->dims;
bounds[0].x = 0 - origin->x;
bounds[0].y = 0 - origin->y;
bounds[1].x = (dims->x -1) - origin->x;
bounds[1].y = (dims->y -1) - origin->y;
return bounds;
}
void get_vecs_from_shape(Piece* cpiece, Vector2* blocks){
Vector2* dims = get_shape_dimensions(current_piece);
// Allocate memory for temporary shape copy
int** shape = malloc(dims->y * sizeof(int*));
for(int i = 0; i < dims->y; i++){
shape[i] = malloc(dims->x * sizeof(int));
}
// Copy the current piece shape
for(int i = 0; i < dims->y; i++){
memcpy(shape[i], cpiece->shape[i], dims->x *sizeof(int));
}
Vector2* origin = &current_piece->origin;
int block_count = 0;
for(int i = 0; i < dims->y; i++){
for(int j = 0; j < dims->x; j++){
if (shape[i][j] == 1 && block_count < 4) {
blocks[block_count].x = j - origin->x;
blocks[block_count].y = i - origin->y;
block_count++;
}
}
}
return;
}
// Sort blocks by y, then by x
void sort_blocks(Vector2* blocks){
for(int i = 0; i < 4; i++){
for(int j = i + 1; j < 4; j++){
if(blocks[j].y < blocks[i].y || (blocks[j].y == blocks[i].y && blocks[j].x < blocks[i].x)){
Vector2 tmp = blocks[i];
blocks[i] = blocks[j];
blocks[j] = tmp;
}
}
}
}
Vector2 find_topleftmost_block_from_buffer(){
for (int i = 0; i < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; i++){
for (int j = 0; j < BOARD_WIDTH; j++){
if(piece_mask[i][j]){
return (Vector2) {(float)j,(float)i};
}
}
}
}
void game_tick(){
if(piece_tick()){
return;
}
place_piece();
zero_piece_mask();
populate_board_mask();
clear_lines();
spawn_piece();
}
Color resolve_color(unsigned char colorValue){
switch(colorValue){
case PC_CYAN: return (Color) {0x00, 0xFF, 0xFF, 0xFF};
case PC_YELLOW: return (Color) {0xFF, 0xFF, 0x00, 0xFF};
case PC_PURPLE: return (Color) {0x80, 0x00, 0x80, 0xFF};
case PC_GREEN: return (Color) {0x00, 0xFF, 0x00, 0xFF};
case PC_RED: return (Color) {0xFF, 0x00, 0x00, 0xFF};
case PC_BLUE: return (Color) {0x00, 0x00, 0xFF, 0xFF};
case PC_ORANGE: return (Color) {0xFF, 0xA5, 0x00, 0xFF};
default: return (Color) {0xFF, 0xFF, 0xFF, 0xFF};
}
}
void debugDraw(int x, int y, Color color){
#ifndef NDEBUG
return;
#endif
DrawRectangle(x * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, y * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, BLOCK_SIZE, BLOCK_SIZE, color);
}
void createSecondWindow(){
}
void draw(){
ClearBackground(RAYWHITE);
for(int i = 0; i < BOARD_HEIGHT; i++){
for(int j = 0; j < BOARD_WIDTH; j++){
if(piece_mask[i+PIECE_BUFFER_OFFSET][j]){
unsigned char pieceColorValue = current_piece->color;
Color drawColor = resolve_color(pieceColorValue);
DrawRectangle(j * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, i * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, BLOCK_SIZE, BLOCK_SIZE, drawColor);
continue;
}
if(board[i][j] != 0){
unsigned char boardColorValue = board[i][j];
Color drawColor = resolve_color(boardColorValue);
DrawRectangle(j * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, i * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, BLOCK_SIZE, BLOCK_SIZE, drawColor);
continue;
}
// background
DrawRectangle(j * GRID_SCREEN_PIXELS, i * GRID_SCREEN_PIXELS, GRID_SCREEN_PIXELS, GRID_SCREEN_PIXELS, COLOR_GRID_FG);
DrawRectangle(j * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, i * GRID_SCREEN_PIXELS + 1 * PIXEL_SCALE, BLOCK_SIZE, BLOCK_SIZE, COLOR_GRID_BG);
}
}
}
int milis_elapsed = 0;
int next_tick = 0;
void move_piece_left(){
// Perform checks
for(int iter_y = 0; iter_y < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; iter_y++){
if(piece_mask[iter_y][0]){
return; // piece would go out of bounds
}
for(int iter_x = 0; iter_x < BOARD_WIDTH; iter_x++){
bool board_bit = board_mask[iter_y-PIECE_BUFFER_OFFSET][iter_x - 1];
bool piece_bit = piece_mask[iter_y][iter_x];
if(board_bit && piece_bit){
return; // collision
}
}
}
// Move Piece
for(int iter_x = 0; iter_x < BOARD_WIDTH; iter_x++){
for(int iter_y = 0; iter_y < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; iter_y++){
if(piece_mask[iter_y][iter_x]){
piece_mask[iter_y][iter_x-1] = 1;
piece_mask[iter_y][iter_x] = 0;
}
}
}
}
void move_piece_right(){
// Perform checks
for(int iter_x = 0; iter_x < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; iter_x++){
if(piece_mask[iter_x][BOARD_WIDTH - 1]){
return; // piece would go out of bounds
}
for(int iter_y = 0; iter_y < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; iter_y++){
bool board_bit = board_mask[iter_y - PIECE_BUFFER_OFFSET][iter_x+1];
bool piece_bit = piece_mask[iter_y][iter_x];
if(board_bit && piece_bit){
return; // collision
}
}
}
// Move piece
for(int j = BOARD_WIDTH - 1; j >= 0; j--){
for(int i = 0; i < BOARD_HEIGHT + PIECE_BUFFER_OFFSET; i++){
if(piece_mask[i][j]){
int new_x = j + 1;
int new_y = i;
if(board_mask[new_y - PIECE_BUFFER_OFFSET][new_x]){
return; // collision
}
piece_mask[i][j+1] = piece_mask[i][j];
piece_mask[i][j] = 0;
}
}
}
}
void update(){
bool accelerate = 0;
int accel_tick_time = 75;
// Update game state
if (IsKeyPressed(KEY_W)) {
// Rotate the current piece
turn_piece(true);
}
if (IsKeyPressed(KEY_A)) {
// Move piece left
move_piece_left();
}
if (IsKeyPressed(KEY_D)) {
// Move piece right
move_piece_right();
}
if (IsKeyDown(KEY_S)) {
// Accelerate piece down
accelerate = 1;
if (next_tick > milis_elapsed + accel_tick_time)
{
game_tick();
}
}
if(milis_elapsed > next_tick){
game_tick();
next_tick = milis_elapsed + (accelerate ? accel_tick_time : 1000);
}
milis_elapsed += GetFrameTime() * 1000;
}
Vector2* get_shape_dimensions(Piece* piece){
static Vector2 dims;
dims = piece->dims;
return &dims;
}
// Helper function to create 2D array from 1D data
int** create_shape_from_array(int rows, int cols, int* data) {
int** shape = malloc(rows * sizeof(int*));
for(int i = 0; i < rows; i++) {
shape[i] = malloc(cols * sizeof(int));
for(int j = 0; j < cols; j++) {
shape[i][j] = data[i * cols + j];
}
}
return shape;
}
void free_shape(int** shape, int rows) {
if(shape) {
for(int i = 0; i < rows; i++) {
free(shape[i]);
}
free(shape);
}
}