리메이크 중/C,C++ 실습 중심

C(&C++) 실습 13. 테트리스 개작

라이피 (Lypi) 2023. 1. 16. 18:55
반응형


출처

soen.kr


#include "Turboc.h"

//키 바인딩
#define LEFT 75
#define RIGHT 77
#define UP 72
#define DOWN 80
#define ESC 27
#define PGUP 73
#define PGDN 81

//게임판 좌 상단 좌표와 게임판의 넓이와 높이
#define BX 5
#define BY 1
#define BW 10
#define BH 20

void DrawScreen();
void DrawBoard();
bool ProcessKey();
void PrintBrick(bool show);
int GetAround(int x, int y, int b, int r);
bool MoveDown();
void TestFull();
void DrawNext();
void PrintInfo();

//좌표를 나타내는 구조체
struct Point {
	int x, y;
};

//벽돌 모양을 정의한 배열
//마지막에 전통 테트리스와 다른 3가지 모양이 추가되었다.
Point Shape[][4][4] = {
     { {0,0,1,0,2,0,-1,0}, {0,0,0,1,0,-1,0,-2}, {0,0,1,0,2,0,-1,0}, {0,0,0,1,0,-1,0,-2} },
     { {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} },
     { {0,0,-1,0,0,-1,1,-1}, {0,0,0,1,-1,0,-1,-1}, {0,0,-1,0,0,-1,1,-1}, {0,0,0,1,-1,0,-1,-1} },
     { {0,0,-1,-1,0,-1,1,0}, {0,0,-1,0,-1,1,0,-1}, {0,0,-1,-1,0,-1,1,0}, {0,0,-1,0,-1,1,0,-1} },
     { {0,0,-1,0,1,0,-1,-1}, {0,0,0,-1,0,1,-1,1}, {0,0,-1,0,1,0,1,1}, {0,0,0,-1,0,1,1,-1} },
     { {0,0,1,0,-1,0,1,-1}, {0,0,0,1,0,-1,-1,-1}, {0,0,1,0,-1,0,-1,1}, {0,0,0,-1,0,1,1,1} },
     { {0,0,-1,0,1,0,0,1}, {0,0,0,-1,0,1,1,0}, {0,0,-1,0,1,0,0,-1}, {0,0,-1,0,0,-1,0,1} },
     { {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0}},
     { {0,0,0,0,0,0,0,1}, {0,0,0,0,0,0,1,0}, {0,0,0,0,0,0,0,1}, {0,0,0,0,0,0,1,0}},
     { {0,0,0,0,0,-1,1,0},{0,0,0,0,-1,0,0,-1},{0,0,0,0,0,1,-1,0},{0,0,0,0,0,1,1,0} },
};

//게임판 상태 표시
enum {EMPTY, BRICK, WALL};
//게임판에 표시할 문자. 위의 열거형 값과 동일한 순서로 맞췄다.
const char* arTile[][3] = 
    { {". ","■", "□"}, 
      {"  ","■", "□"},
      {"  ","##", "||"},
      {". ","●", "▩"},
    };
//arTile[EMPTY] == ". " 이다.
int ttype = 0;

//외벽을 포함한 게임판의 상태를 기록한다.
//x축 y축의 순서이며 넓이가 x축에 기록되었으므로 
//반시계 방향으로 90도 돌아가게 기록될 것이다.
int board[BW + 2][BH + 2];

//이동중인 벽돌의 배열상의 현재 좌표이다.
//화면상의 좌표로 변환하려면 BX+nx*2, BY+ny식을 쓰면 된다. 
//벽돌 하나가 x축(넓이) 두칸, y축 (높이) 한칸을 차지하기 때문이다. 
//"::"이런 느낌이다.
int nx, ny;

//이동중인 벽돌의 번호와 회전번호를 나타낸다.
int brick, rot;

//다음에 나올 벽돌을 미리 저장한다.
int nbrick;

//점수와 총 벽돌 갯수
int score, bricksum;

int main() {

    setcursortype(NOCURSOR);
    randomize();

    while (3) {
    clrscr();

    //게임 초기화 (보드 배열을 초기화한다.)
    for (int x = 0; x < BW + 2; x++) {
        for (int y = 0; y < BH + 2; y++) {
            board[x][y] = (y == 0 || y == BH + 1 || x == 0 || x == BW + 1) ? WALL : EMPTY;
        }
    }
    //보드를 그린다.
    DrawScreen();
    int nFrame = 20;
    score = 0;
    bricksum = 0;

    nbrick = random(sizeof(Shape) / sizeof(Shape[0]));
        while (1) { //첫번째 루프
            bricksum++;
            brick = nbrick;
            //랜덤으로 새 벽돌 생성
            nbrick = random(sizeof(Shape) / sizeof(Shape[0]));
            DrawNext();
            nx = BW / 2;        //x좌표는 게임판 가운데
            ny = 3;             //y좌표는 위에서 3번째 (첫번째는 외벽임)
            rot = 0;           //회전 좌표는 첫번째로
            PrintBrick(true);   //벽돌을 그린다.

            //게임 종료 조건 확인
            //새로 생성한 벽돌의 주변이 공백이 아니라면 게임 오버이므로 루프를 탈출한다.
            //확실한 방법은 아니다.
            if (GetAround(nx, ny, brick, rot) != EMPTY) break;
            int nStay = nFrame;

            while (2) { //두번째 루프 
                //벽돌 하나를 처리하는 루프이다.

                //프레임이 지나면 벽돌을 내린다.
                //초당 한칸씩 내려온다.
                if (--nStay == 0) {
                    nStay = nFrame;
                    //MoveDown() 함수는 벽돌이 바닥에 닿으면 true를 리턴한다.
                    if (MoveDown()) break;
                }
                //키 입력 처리를 한다.
                //ProcessKey() 함수도 벽돌이 바닥에 닿으면 true를 리턴한다.
                if (ProcessKey()) break;

                //if문이 true가 되면 2번째 루프를 빠져나간다.

                //시간을 지연시킨다.
                //20fps이다.
                delay(1000 / nFrame);
            }
            if (bricksum % 10 == 0 && nFrame > 5) {
                nFrame--;
            }
        }

        //게임 오버 처리
        clrscr();
        gotoxy(30, 12); puts(" G A M E   O V E R");
        gotoxy(25, 14); puts("다시 시작하려면 Y를 누르세요.");
        if (tolower(_getch()) != 'y') break;
    }
    setcursortype(NORMALCURSOR);
}

//화면 전체를 그린다.
//게임판과 게임 이름, 벽까지 한번에 그림.
void DrawScreen()
{
    for (int x = 0; x < BW + 2; x++) {
        for (int y = 0; y < BH + 2; y++) {
            gotoxy(BX + x * 2, BY + y);
            puts(arTile[ttype][board[x][y]]);
        }
    }
    
    gotoxy(50, 3); puts("Tetris Ver 1.1");
    gotoxy(50, 5); puts("좌우:이동, 위:회전, 아래:내림");
    gotoxy(50, 6); puts("공백: 전부 내림, ESC : 종료");
    gotoxy(50, 7); puts("P:정지, PgUp, PgDn : 블럭 그림 변경");

    DrawNext();
    PrintInfo();

}

//안쪽 게임판만 그린다. 
//쌓이는 벽돌도 얘가 그린다.
void DrawBoard()
{
    for (int x = 1; x < BW + 1; x++) {
        for (int y = 1; y < BH + 1; y++) {
            gotoxy(BX + x * 2, BY + y);
            puts(arTile[ttype][board[x][y]]);
        }
    }
}

//키 입력을 처리한다.
//이동중인 벽돌이 바닥에 닿으면 true를 리턴한다.
bool ProcessKey() 
{
    int trot;

    if (_kbhit()) {
        int ch = _getch();
        if (ch == 0xE0 || ch == 0) {
            ch = _getch();
            switch (ch) {
            case LEFT:
                if (GetAround(nx - 1, ny, brick, rot) == EMPTY) {
                    PrintBrick(false);
                    nx--;
                    PrintBrick(true);
                }
                break;

            case RIGHT:
                if (GetAround(nx + 1, ny, brick, rot) == EMPTY) {
                    PrintBrick(false);
                    nx++;
                    PrintBrick(true);
                }
                break;

            case UP:
                trot = (rot == 3 ? 0 : rot + 1);
                if (GetAround(nx, ny, brick, trot) == EMPTY) {
                    PrintBrick(false);
                    rot = trot;
                    PrintBrick(true);
                }
                break;

            case DOWN:
                if (MoveDown()) {
                    return true;
                }
                break;

            case PGUP:
                ttype++;
                if (ttype == sizeof(arTile) / sizeof(arTile[0])) ttype = 0;
                DrawScreen();
                PrintBrick(true);
                break;

            case PGDN:
                if (ttype == 0) ttype = sizeof(arTile) / sizeof(arTile[0]);
                ttype--;
                DrawScreen();
                PrintBrick(true);
                break;
            }
        } else {
            switch (tolower(ch)) {
            case ' ':
                while (MoveDown() == false) { ; }
                return true;
            case ESC:
                clrscr();
                gotoxy(30, 12); puts(" G A M E   O V E R");
                exit(0);
            case 'p':
                clrscr();
                gotoxy(12, 10);
                puts("일시 중지 \n 다시 시작하려면 아무키나 누르세요");
                _getch();
                clrscr();
                DrawScreen();
                PrintBrick(true);
                break;
            }
        }
    }

    return false;
}

//이동중인 벽돌을 그리거나 삭제한다.
//전역 변수를 참조한다.
void PrintBrick(bool Show) 
{
    for (int i = 0; i < 4; i++) {
        gotoxy(BX + (Shape[brick][rot][i].x + nx) * 2, BY + Shape[brick][rot][i].y + ny);
        puts(arTile[ttype][Show ? BRICK : EMPTY]);
    }
}

//인수로 전달된 벽돌 주변에 뭐가 있는지 확인한다.
//이동중인 벽돌 주변을 확인하는게 아니다.
int GetAround(int x, int y, int b, int r) 
{
    int k = EMPTY;

    for (int i = 0; i < 4; i++) {
        k = max(k, board[x + Shape[b][r][i].x][y + Shape[b][r][i].y]);
    }

    return k;
}

//벽돌을 한칸 아래로 이동시킨다.
//만약 바닥에 닿았으면 TestFull()함수를 호출한 후 true를 리턴한다.
bool MoveDown()
{
    if (GetAround(nx, ny + 1, brick, rot) != EMPTY) {
        TestFull();
        return true;
    }

    PrintBrick(false);
    ny++;
    PrintBrick(true);
    return false;
}

//수평으로 다 채워진 줄을 찾아 삭제한다.
void TestFull() {
    int count = 0;
    static int arScore[] = { 0, 1, 3, 8, 20 };

    for (int i = 0; i < 4; i++) {
        board[nx + Shape[brick][rot][i].x][ny + Shape[brick][rot][i].y] = BRICK;
    }

    int x, y;
    for (y = 1; y < BH + 1; y++) {
        for (x = 1; x < BW + 1; x++) {
            if (board[x][y] != BRICK) break;
        }
        if (x == BW + 1) {
            count++;
            for (int ty = y; ty > 1; ty--) {
                for (x = 1; x < BW + 1; x++) {
                    board[x][ty] = board[x][ty - 1];
                }
            }
            DrawBoard();
            delay(200);
        }
    }

    score += arScore[count];
    PrintInfo();

}

void DrawNext() {
    for (int x = 50; x <= 70; x += 2) {
        for (int y = 12; y <= 18; y++) {
            gotoxy(x, y);
            puts(arTile[ttype][x == 50 || x == 70 || y == 12 || y == 18 ? WALL : EMPTY ]);
        }
    }

    for (int i = 0; i < 4; i++) {
        gotoxy(60 + (Shape[nbrick][0][i].x) * 2, 15 + Shape[nbrick][0][i].y);
        puts(arTile[ttype][BRICK]);
    }
}

void PrintInfo() {
    gotoxy(50, 9); printf("점수 : %d 점 \t", score);
    gotoxy(50, 10); printf("총벽돌 : %d 개", bricksum);
}

  ■ 강의의 내용에 따라 조금 바꿔보았다.

  ■ 다음 블럭을 알 수 있는 기능과 일시 정지 및 다시 하기, 점수 기능과 난이도 조절 기능을 넣었다.

반응형