-
Notifications
You must be signed in to change notification settings - Fork 9
/
tetris.c
587 lines (548 loc) · 15.9 KB
/
tetris.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
#include <stdio.h>
#include <stdlib.h>
#include "logsys.h"
#include "input.h"
#include "graphics.h"
// Size of the stage
#define STAGE_W 10
#define STAGE_H 20
// Number of lines to clear before going to the next level
#define LINES_PER_LEVEL 20
// "SPEED" is actually number of frames here
// Initial speed is the "gravity" for level 1
#define INITIAL_SPEED 60
// Gravity for soft drop when player holds the down button
#define DROP_SPEED 4
// Size for each individual block, and also effects a number of other things
#define BLOCK_SIZE 16
// Minimum time a between a piece touching the bottom and locking
#define LOCK_DELAY 30
// For delayed auto shift, wait SHIFT_DELAY frames first,
// then wait SHIFT_SPEED while left/right continues to be held
#define SHIFT_DELAY 20
#define SHIFT_SPEED 4
// Score amounts rewarded for various actions
#define SCORE_SINGLE 100
#define SCORE_DOUBLE 300
#define SCORE_TRIPLE 500
#define SCORE_TETRIS 800
#define SCORE_EZ_TSPIN 100
#define SCORE_EZ_TSPIN_SINGLE 200
#define SCORE_TSPIN 400
#define SCORE_TSPIN_SINGLE 800
#define SCORE_TSPIN_DOUBLE 1200
#define SCORE_SOFT_DROP 1
#define SCORE_HARD_DROP 2
// Game mode, like using screens except a single variable switch instead
#define MODE_TITLE 0
#define MODE_OPTIONS 1
#define MODE_STAGE 2
#define MODE_GAMEOVER 3
// Locations and sizes that depend on the chosen block size
#define STAGE_X 6 * BLOCK_SIZE
#define STAGE_Y 2 * BLOCK_SIZE
#define SCREEN_W 22 * BLOCK_SIZE
#define SCREEN_H 24 * BLOCK_SIZE
#define QUEUE_X 17 * BLOCK_SIZE
#define QUEUE_Y 2 * BLOCK_SIZE
#define HOLD_X 1 * BLOCK_SIZE
#define HOLD_Y 2 * BLOCK_SIZE
typedef unsigned char Uint8;
typedef signed char Sint8;
typedef unsigned short Uint16;
typedef unsigned int Uint32;
typedef unsigned char bool;
enum {false,true};
// This array describes the block configuration of a piece, for each shape
// and rotation in a 4x4 grid ordered left to right, then top to bottom
// Indexed: PieceDB[type][flip]
Uint16 PieceDB[7][4] = { // O, I, L, J, S, Z, T
{0b0000011001100000,0b0000011001100000,0b0000011001100000,0b0000011001100000},
{0b0100010001000100,0b0000111100000000,0b0010001000100010,0b0000000011110000},
{0b0110010001000000,0b0000111000100000,0b0100010011000000,0b1000111000000000},
{0b0100010001100000,0b0000111010000000,0b1100010001000000,0b0010111000000000},
{0b0110110000000000,0b0100011000100000,0b0000011011000000,0b1000110001000000},
{0b1100011000000000,0b0010011001000000,0b0000110001100000,0b0100110010000000},
{0b0100111000000000,0b0100011001000000,0b0000111001000000,0b0100110001000000}
};
// Helper function to single out a block based on x and y position
Uint16 blockmask(int x, int y) { return 0x8000>>(x+y*4); }
Uint32 PieceColor[8] = {
COLOR_YELLOW, // O - Yellow
COLOR_CYAN, // I - Cyan
COLOR_BLUE, // J - Blue
COLOR_ORANGE, // L - Orange
COLOR_GREEN, // S - Green
COLOR_RED, // Z - Red
COLOR_PURPLE, // T - Purple
COLOR_SHADOW // Shadow
};
// Represents an "instance" of a piece
typedef struct {
// X and Y position (in blocks) on the stage
Sint8 x; Sint8 y;
// Type and flip value to index the PieceDB array
Uint8 type; Uint8 flip;
} Piece;
// Current game mode (title screen, stage, game over screen, etc)
int gameMode = MODE_STAGE;
// Contains blocks/pieces that have fallen and bacame part of the stage
Uint8 stage[STAGE_W][STAGE_H];
// Random bag used to decide piece order
Uint8 randomBag[7], bagCount = 0;
// Block speed is the falling speed measured in frames between motions
// The block time is the elapsed frames which counts up to block speed
int blockSpeed = INITIAL_SPEED, blockTime = 0;
// Player's stats, score, level, etc
int score = 0, linesCleared = 0, totalLines = 0, level = 1, nextLevel = LINES_PER_LEVEL;
// Current piece controlled by player, held for later, and the queue
Piece piece, hold, queue[5];
// Whether the player held the previous piece, and has held any piece yet
bool holded = false, heldSomething = false;
// Whether game is paused or running. Not running means the game will exit
bool paused = false, running = true;
// True if the player is holding down to soft drop a piece
bool dropping = false;
// Frame count for delayed auto shift, and the direction the piece is being shifted
int autoShift = SHIFT_DELAY, shiftDirection = 0;
// Function prototypes and order
void initialize();
void reset_game();
void fill_random_bag();
void move_piece_left();
void move_piece_right();
void move_piece_down();
void rotate_piece_left();
void rotate_piece_right();
void hard_drop();
void hold_piece();
void lock_piece();
bool validate_piece(Piece p);
bool check_lock(Piece p);
bool detect_tspin(Piece p);
bool wall_kick(Piece *p);
Piece ghost_piece(Piece p);
void reset_speed();
void clear_row();
void next_piece();
void update();
void update_stage();
void update_game_over();
void draw();
void draw_piece(Piece p, int x, int y, bool shadow);
void draw_stage();
void draw_game_over();
// Entry point, args are unused but SDL complains if they are missing
int main(int argc, char *argv[]) {
log_open("error.log");
initialize();
log_msgf(INFO, "Startup success.\n");
while(running) {
update();
draw();
}
graphics_quit();
log_msgf(INFO, "Process exited cleanly.\n");
log_close();
return 0;
}
// Create the game window and start stuff
void initialize() {
graphics_init(SCREEN_W, SCREEN_H);
graphics_load_font("data/DejaVuSerif.ttf");
reset_game();
}
// Put values back to their defaults and start over
void reset_game() {
// Clear the stage
for(int i = 0; i < STAGE_W; i++) {
for(int j = 0; j < STAGE_H; j++) {
stage[i][j] = 0;
}
}
// reset bag, queue, piece
fill_random_bag();
piece.type = randomBag[bagCount++];
piece.flip = 0;
piece.y = -2;
piece.x = 3;
for(int i = 0; i < 5; i++) {
queue[i].type = randomBag[bagCount++];
queue[i].flip = 0;
}
// Default values
heldSomething = false;
holded = false;
score = 0;
level = 0;
nextLevel = LINES_PER_LEVEL;
linesCleared = 0;
totalLines = 0;
reset_speed();
next_piece();
gameMode = MODE_STAGE;
}
// Regenerate the random bag, it contains the next 7 pieces to go in the queue
// It always contains one of each type of tetromino
void fill_random_bag() {
Uint8 pool[7] = { 0, 1, 2, 3, 4, 5, 6 };
for(int i = 0; i < 7; i++) {
int j = rand() % (7 - i);
randomBag[i] = pool[j];
for(; j < 6; j++) {
pool[j] = pool[j+1];
}
}
bagCount = 0;
log_msgf(TRACE, "FillBag: %hhu, %hhu, %hhu, %hhu, %hhu, %hhu, %hhu\n",
randomBag[0], randomBag[1], randomBag[2], randomBag[3],
randomBag[4], randomBag[5], randomBag[6]);
}
// Move to the left if possible
void move_piece_left() {
Piece p = piece;
p.x--;
if(validate_piece(p)) {
piece = p;
// Reset timer if next fall will lock
if(check_lock(p)) blockTime = 0;
}
}
// Move to the right if possible
void move_piece_right() {
Piece p = piece;
p.x++;
if(validate_piece(p)) {
piece = p;
// Reset timer if next fall will lock
if(check_lock(piece)) blockTime = 0;
}
}
// Move down or lock
void move_piece_down() {
if(check_lock(piece)) {
lock_piece(piece);
} else {
piece.y++;
if(dropping) score += SCORE_SOFT_DROP;
}
blockTime = 0;
}
// Rotate to the left if possible
void rotate_piece_left() {
Piece p = piece;
if(p.flip == 0) p.flip = 3; else p.flip--;
// If rotating makes the piece overlap, try to wall kick
if(validate_piece(p) || wall_kick(&p)) {
piece = p;
// Reset timer if next fall will lock
if(check_lock(piece)) blockTime = 0;
}
}
// Rotate to the right if possible
void rotate_piece_right() {
Piece p = piece;
if(p.flip == 3) p.flip = 0; else p.flip++;
// If rotating makes the piece overlap, try to wall kick
if(validate_piece(p) || wall_kick(&p)) {
piece = p;
// Reset timer if next fall will lock
if(check_lock(piece)) blockTime = 0;
}
}
// Drop piece to the bottom and lock it
void hard_drop() {
while (!check_lock(piece)) {
piece.y++;
score += SCORE_HARD_DROP;
}
lock_piece();
}
// Switch current and hold block
void hold_piece() {
if(holded) return; // Don't hold twice in a row
piece.x = 3; piece.y = 0;
if(heldSomething) {
Piece temp = piece;
piece = hold;
hold = temp;
} else {
hold = piece;
heldSomething = true;
next_piece();
}
holded = true;
}
// Lock piece into stage and spawn the next
void lock_piece() {
// Push piece data into stage
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
if(PieceDB[piece.type][piece.flip]&blockmask(i, j)) {
stage[piece.x+i][piece.y+j] = piece.type+1;
}
}
}
// Clear any completed rows
int rows_cleared = 0;
for(int i = 0; i < STAGE_H; i++) {
int filled = 0;
for(int j = 0; j < STAGE_W; j++) if (stage[j][i] > 0) filled++;
if (filled == STAGE_W) {
clear_row(i);
rows_cleared++;
}
}
// Score rewards
int reward = 0;
// 3-corner T-spin
if(piece.type == 7 && detect_tspin(piece)) {
switch(rows_cleared) {
case 0: reward += SCORE_TSPIN * level; break;
case 1: reward += SCORE_TSPIN_SINGLE * level; break;
case 2: reward += SCORE_TSPIN_DOUBLE * level; break;
}
} else {
// Immobile (EZ) T-spin
Piece p = piece;
if(piece.type == 7 && wall_kick(&p)) {
switch(rows_cleared) {
case 0: reward += SCORE_EZ_TSPIN * level; break;
case 1: reward += SCORE_EZ_TSPIN_SINGLE * level; break;
}
} else {
// Rows clear, no T-spin
switch (rows_cleared) {
case 1: reward += SCORE_SINGLE * level; break;
case 2: reward += SCORE_DOUBLE * level; break;
case 3: reward += SCORE_TRIPLE * level; break;
case 4: reward += SCORE_TETRIS * level; break;
}
}
}
score += reward;
// Update line total and level
linesCleared += rows_cleared;
totalLines += rows_cleared;
nextLevel -= rows_cleared;
if (nextLevel <= 0) {
nextLevel += LINES_PER_LEVEL;
level++;
}
next_piece();
}
// Checks if the piece is overlapping with anything
bool validate_piece(Piece p) {
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
int x = p.x + i, y = p.y + j;
if(PieceDB[p.type][p.flip]&blockmask(i, j)) {
if (x < 0 || x >= STAGE_W || y >= STAGE_H) return false;
if (y >= 0 && stage[x][y] > 0) return false;
}
}
}
return true;
}
// Check if piece can be moved down any further
bool check_lock(Piece p) {
p.y++;
return !validate_piece(p);
}
bool detect_tspin(Piece p) {
return (stage[p.x, p.y] > 0) + (stage[p.x + 2, p.y] > 0) +
(stage[p.x + 2, p.y + 2] > 0) + (stage[p.x, p.y + 2] > 0) == 3;
}
// Try to push the piece out of an obstacles way
bool wall_kick(Piece *p) {
// Left
p->x -= 1;
if(validate_piece(*p)) return true;
// Right
p->x += 2;
if(validate_piece(*p)) return true;
// Up
p->x -= 1;
p->y -= 1;
if(validate_piece(*p)) return true;
// Unable to wall kick, return piece to the way it was
p->y += 1;
return false;
}
// Returns a shadow to display where the piece will drop
Piece ghost_piece(Piece p) {
while(!check_lock(p)) p.y++;
return p;
}
// Adjusts the fall speed based on the current level
void reset_speed() {
blockSpeed = INITIAL_SPEED - (level * 5);
if(blockSpeed < DROP_SPEED) blockSpeed = DROP_SPEED;
}
// Clear a row and move down above rows
void clear_row(int row) {
for(int i = row; i > 0; i--) {
for(int j = 0; j < STAGE_W; j++) {
stage[j][i] = stage[j][i-1];
}
}
for(int j = 0; j < STAGE_W; j++) stage[0][j] = 0;
}
// Shift to the next block in the queue
void next_piece() {
piece = queue[0];
piece.y = -2;
piece.x = 3;
for(int i = 0; i < 4; i++) queue[i] = queue[i+1];
// Grab piece from the bag, refill if it becomes empty
queue[4].type = randomBag[bagCount++];
if(bagCount == 7) fill_random_bag();
holded = false; // Allow player to hold the next piece
// End the game if the next piece overlaps
if(!validate_piece(piece)) gameMode = MODE_GAMEOVER;
reset_speed();
}
// Main update, handles events and calls relevant game mode update function
void update() {
// Update keyboard input and events
// Close the game if the window is closed or escape key is pressed
if(input_update() || key.esc) running = false;
switch(gameMode) {
case MODE_STAGE:
update_stage();
break;
case MODE_GAMEOVER:
update_game_over();
break;
}
}
// Update actions when the game is being played
void update_stage() {
if(key.enter && !oldKey.enter) paused = !paused;
// Don't update the rest if the game is paused
if(paused) return;
// Moving left and right
if(key.left && !oldKey.left) {
move_piece_left();
shiftDirection = -1;
autoShift = SHIFT_DELAY;
} else if(key.right && !oldKey.right) {
move_piece_right();
shiftDirection = 1;
autoShift = SHIFT_DELAY;
}
// Delayed Auto Shift
if(key.right - key.left == shiftDirection) {
autoShift--;
if(autoShift == 0) {
autoShift = SHIFT_SPEED;
if(key.left) move_piece_left();
else if(key.right) move_piece_right();
}
}
// Rotating block
if(key.z && !oldKey.z) rotate_piece_left();
if(key.x && !oldKey.x) rotate_piece_right();
if(key.up && !oldKey.up) rotate_piece_right();
// Drop and Lock
if(key.space && !oldKey.space) hard_drop();
// Hold a block and save it for later
if(key.shift && !oldKey.shift) hold_piece();
// If we hold the down key fall faster
if(key.down && !oldKey.down) {
blockSpeed = DROP_SPEED;
dropping = true;
move_piece_down();
} else if(!key.down && oldKey.down) {
reset_speed();
dropping = false;
}
// Push block down according to speed
blockTime++;
if(blockTime >= blockSpeed) {
// No matter the gravity, always wait at least half a second
// before locking
if(!check_lock(piece) || blockTime >= LOCK_DELAY || key.down) {
move_piece_down();
}
}
}
// Game over screen
void update_game_over() {
if(key.enter && !oldKey.enter) reset_game();
}
void draw() {
graphics_set_color(COLOR_BLACK);
// Draw stage background
graphics_draw_rect(STAGE_X, STAGE_Y, STAGE_W * BLOCK_SIZE, STAGE_H * BLOCK_SIZE);
// Queue background
graphics_draw_rect(QUEUE_X, QUEUE_Y, BLOCK_SIZE * 4, BLOCK_SIZE * 4 * 5);
// Hold background
graphics_draw_rect(HOLD_X, HOLD_Y, BLOCK_SIZE * 4, BLOCK_SIZE * 4);
// Game mode specific draw functions
switch(gameMode) {
case MODE_STAGE:
draw_stage();
break;
case MODE_GAMEOVER:
draw_game_over();
break;
}
graphics_set_color(COLOR_BLACK);
// Draw the text
graphics_draw_string("Score: ", STAGE_X, 0);
graphics_draw_int(score, STAGE_X + graphics_string_width("Score: ") + 96, 0);
graphics_draw_string("Queue", QUEUE_X, 0);
graphics_draw_string("Hold", HOLD_X, 0);
graphics_draw_string("Level:", HOLD_X, HOLD_Y + (5 * BLOCK_SIZE));
graphics_draw_int(level, HOLD_X + 64, HOLD_Y + (7 * BLOCK_SIZE));
graphics_draw_string("Next:", HOLD_X, HOLD_Y + (10 * BLOCK_SIZE));
graphics_draw_int(nextLevel, HOLD_X + 64, HOLD_Y + (12 * BLOCK_SIZE));
graphics_draw_string("Total:", HOLD_X, HOLD_Y + (15 * BLOCK_SIZE));
graphics_draw_int(totalLines, HOLD_X + 64, HOLD_Y + (17 * BLOCK_SIZE));
// Wait until frame time and flip the backbuffer
graphics_flip();
}
void draw_piece(Piece p, int x, int y, bool shadow) {
// 7 is the shadow color, others match with Piece.type
int c = shadow ? 7 : p.type;
graphics_set_color(PieceColor[c]);
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
if(PieceDB[p.type][p.flip]&blockmask(i, j)) {
if(p.y + j < 0) continue;
graphics_draw_rect(x + i * BLOCK_SIZE + 1, y + j * BLOCK_SIZE + 1,
BLOCK_SIZE - 2, BLOCK_SIZE - 2);
}
}
}
}
void draw_stage() {
// Draw the pieces on the stage
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 20; j++) {
if (stage[i][j] == 0) continue;
int c = stage[i][j] - 1;
graphics_set_color(PieceColor[c]);
graphics_draw_rect(i * BLOCK_SIZE + STAGE_X + 1,
j * BLOCK_SIZE + STAGE_Y + 1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
}
}
// Draw the ghost piece (shadow)
Piece shadow = ghost_piece(piece);
draw_piece(shadow, shadow.x * BLOCK_SIZE + STAGE_X, shadow.y * BLOCK_SIZE + STAGE_Y, true);
// Draw current piece
draw_piece(piece, piece.x * BLOCK_SIZE + STAGE_X, piece.y * BLOCK_SIZE + STAGE_Y, false);
// Queue pieces
for(int q = 0; q < 5; q++) {
draw_piece(queue[q], QUEUE_X, q * (BLOCK_SIZE*4) + QUEUE_Y, false);
}
// Hold piece
if(heldSomething) {
draw_piece(hold, HOLD_X, HOLD_Y, false);
}
}
void draw_game_over() {
graphics_set_color(COLOR_RED);
graphics_draw_string("Game Over", STAGE_X, STAGE_Y + 5*BLOCK_SIZE);
}