Created
September 18, 2011 05:42
-
-
Save rygorous/1224778 to your computer and use it in GitHub Desktop.
Minitris
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <ctype.h> | |
#include <time.h> | |
// ---- platform specific part | |
enum { | |
CURSOR_LEFT = 256, | |
CURSOR_RIGHT, | |
CURSOR_UP, | |
CURSOR_DOWN, | |
}; | |
#ifdef _WIN32 | |
#define WIN32_LEAN_AND_MEAN | |
#include <Windows.h> | |
#include <conio.h> | |
static HANDLE conout; | |
static void set_cursor_visible(BOOL flag) | |
{ | |
CONSOLE_CURSOR_INFO cursor; | |
GetConsoleCursorInfo(conout, &cursor); | |
cursor.bVisible = flag; | |
SetConsoleCursorInfo(conout, &cursor); | |
} | |
void term_init() | |
{ | |
conout = CreateFileA("CONOUT$", GENERIC_READ|GENERIC_WRITE, 0, NULL, 0, 0, NULL); | |
set_cursor_visible(FALSE); | |
} | |
void term_close() | |
{ | |
set_cursor_visible(TRUE); | |
} | |
void term_clear() | |
{ | |
CONSOLE_SCREEN_BUFFER_INFO info; | |
COORD origin = { 0, 0 }; | |
GetConsoleScreenBufferInfo(conout, &info); | |
FillConsoleOutputCharacterA(conout, ' ', info.dwSize.X * info.dwSize.Y, origin, NULL); | |
} | |
void term_move_cursor(int x, int y) | |
{ | |
COORD pos = { x, y }; | |
SetConsoleCursorPosition(conout, pos); | |
} | |
int term_getkey() | |
{ | |
if (_kbhit()) { | |
int ch = _getch(); | |
if (ch == 0xe0) { // escape | |
switch (_getch()) { | |
case 0x4b: return CURSOR_LEFT; | |
case 0x4d: return CURSOR_RIGHT; | |
case 0x48: return CURSOR_UP; | |
case 0x50: return CURSOR_DOWN; | |
} | |
} else | |
return ch; | |
} | |
else | |
return 0; | |
return _kbhit() ? _getch() : 0; | |
} | |
#else | |
#error Implement terminal interface for this platform! | |
#endif | |
// ---- actual game | |
typedef unsigned char U8; | |
struct Block { | |
U8 x, y; // 4 x/y coordinates, 2 bits each | |
}; | |
static const Block tetrominos[] = { // I, J, L, O, S, Z, T | |
{ 0xaa, 0xe4 }, { 0x6a, 0xf9 }, { 0xea, 0xf9 }, { 0x99, 0xa5 }, | |
{ 0x9e, 0xa5 }, { 0x94, 0xa5 }, { 0xb9, 0x95 }, | |
}; | |
static char playfield[4+20+1][3+10+3]; | |
static Block current, next; | |
static int current_x, current_y; | |
static long score, lines, level; | |
static int unpack(U8 coord, int i) | |
{ | |
return (coord >> (2*i)) & 3; | |
} | |
static Block rotate_block(Block in, bool ccw) | |
{ | |
Block o = { ccw ? in.y : ~in.y, ccw ? ~in.x : in.x }; | |
return o; | |
} | |
static Block random_tetromino() | |
{ | |
Block b = tetrominos[rand() % 7]; | |
int nrot = rand() % 4; | |
while (nrot--) | |
b = rotate_block(b, true); | |
return b; | |
} | |
static bool try_move(int dx, int dy) | |
{ | |
int x = current_x + dx, y = current_y + dy; | |
if (x < 0 || x >= 3+10 || y < 0) | |
return false; | |
for (int i=0; i<4; i++) | |
if (playfield[y + unpack(current.y, i)][x + unpack(current.x, i)] != '.') | |
return false; | |
current_x = x; | |
current_y = y; | |
return true; | |
} | |
static void rotate(bool ccw) | |
{ | |
current = rotate_block(current, ccw); | |
static const int jiggle[][2] = { {0,0}, {-1,0}, {1,0}, {0,-1}, {-1,-1}, {1,-1}, {-2,0}, {2,0} }; | |
for (int i=0; i < sizeof(jiggle)/sizeof(*jiggle); i++) | |
if (try_move(jiggle[i][0], jiggle[i][1])) | |
return; | |
// no legal move possible if we do this rotation, so undo it | |
current = rotate_block(current, !ccw); | |
} | |
static void put_tetromino(char ch) | |
{ | |
for (int i=0; i<4; i++) | |
playfield[current_y + unpack(current.y, i)][current_x + unpack(current.x, i)] = ch; | |
} | |
static void draw_playfield() | |
{ | |
put_tetromino('@'); | |
for (int y=4; y<25; y++) { | |
term_move_cursor(2, y-4); | |
puts(playfield[y]+2); | |
} | |
put_tetromino('.'); | |
} | |
static void delay(double time) | |
{ | |
clock_t timeout = clock() + (clock_t) (time * CLOCKS_PER_SEC); | |
while (clock() < timeout); | |
} | |
static void drop() | |
{ | |
if (try_move(0, 1)) | |
return; | |
put_tetromino('#'); | |
int full = 0, nlines = 0, x, y=current_y; | |
for (int i=0; i<4; i++) { | |
for (x=3; playfield[y+i][x] == '#'; x++); | |
if (x == 3+10) { // line is full | |
full |= 1 << i; | |
nlines++; | |
} | |
} | |
current_y = 0; // doubles as a marker for "no active block" | |
if (full) { | |
for (int flash=0; flash<4; flash++) { | |
for (int i=0; i<4; i++) | |
if (full & (1 << i)) | |
memset(&playfield[y+i][3], (flash & 1) ? '.' : '#', 10); | |
draw_playfield(); | |
delay(0.25); | |
} | |
for (int i=0; i<4; i++) | |
if (full & (1 << i)) | |
memmove(playfield[4], playfield[3], (y+i-3) * sizeof(playfield[0])); | |
static const long scores[] = { 0, 100, 200, 400, 800 }; | |
lines += nlines; | |
score += scores[nlines]; | |
} | |
} | |
static void draw_next() | |
{ | |
char str[7][7] = { | |
" Next ", "+----+", "| |", "| |", | |
"| |", "| |", "+----+" | |
}; | |
for (int i=0; i<4; i++) | |
str[2 + unpack(next.y, i)][1 + unpack(next.x, i)] = '@'; | |
for (int i=0; i<7; i++) { | |
term_move_cursor(16, 14+i); | |
puts(str[i]); | |
} | |
} | |
static void draw_score() | |
{ | |
static const char *desc[] = { "Score", "Lines", "Level" }; | |
long values[] = { score, lines, level }; | |
for (int i=0; i<3; i++) { | |
term_move_cursor(16, i*3); | |
puts(desc[i]); | |
term_move_cursor(16, i*3+1); | |
printf("%9ld", values[i]); | |
} | |
} | |
static bool game_over(const char *msg) | |
{ | |
term_clear(); | |
term_move_cursor(10, 10); | |
puts(msg); | |
term_move_cursor(10, 11); | |
printf("Final score: %ld", score); | |
term_move_cursor(10, 13); | |
puts("Continue? (Y/N)"); | |
for (;;) { | |
switch (term_getkey()) { | |
case 'y': case 'Y': return true; | |
case 'n': case 'N': | |
case 27: return false; | |
} | |
} | |
} | |
static bool game() | |
{ | |
term_clear(); | |
// init the playfield | |
memset(playfield, '.', sizeof(playfield)); | |
for (int y=0; y<25; y++) { | |
playfield[y][2] = '*'; | |
playfield[y][13] = '*'; | |
playfield[y][14] = 0; | |
} | |
for (int x=2; x<14; x++) | |
playfield[24][x] = '*'; | |
clock_t ref_time, timeout; | |
bool dropped = true; | |
score = lines = 0; | |
next = random_tetromino(); | |
for (;;) { | |
level = lines/10 + 1; | |
if (dropped) { | |
timeout = CLOCKS_PER_SEC * (22 - level) / 21; | |
if (!timeout) timeout = 1; | |
ref_time = clock(); | |
dropped = false; | |
} | |
if (!current_y) { | |
current = next; | |
next = random_tetromino(); | |
current_x = 6; | |
current_y = 2; | |
if (!try_move(0, 0)) | |
return game_over("You lose!"); | |
timeout *= 2; // extra pause on a new block | |
} | |
draw_score(); | |
draw_next(); | |
draw_playfield(); | |
int key=0; | |
while (!key) { | |
key = term_getkey(); | |
if (!key && clock() >= ref_time + timeout) | |
key = CURSOR_DOWN; | |
} | |
switch (key) { | |
case CURSOR_LEFT: try_move(-1, 0); break; | |
case CURSOR_RIGHT: try_move( 1, 0); break; | |
case CURSOR_DOWN: drop(); dropped = true; break; | |
case 'a': case 'A': rotate(true); break; | |
case CURSOR_UP: | |
case 'd': case 'D': rotate(false); break; | |
case ' ': while (current_y) drop(); dropped = true; break; | |
case 27: return false; | |
} | |
if (lines >= 200) | |
return game_over("You win!"); | |
} | |
} | |
int main() | |
{ | |
term_init(); | |
while (game()); | |
term_close(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment