Posted on

C言語初級者がMacのコンソールで実行可能なテトリスを作ってみた

C言語初級者がMacのコンソールで実行できるテトリスを作ってみました。参考にした動画はこちらです。テトリスについてはWikipediaも参考にしました。この投稿では作ってみた上で気になった箇所をピックアップして解説していきます。全ソースコードはこちらで確認できます。これについては、Youtubeにあげている方にも許可を頂いています。

テトリス – Wikipedia

日本では、 1988年にセガ・エンタープライゼス(後の セガ・インタラクティブ)から発売された アーケード版( セガ・システム16版)の人気により浸透した。当時はまだ操作法が確立されていなかったが、このシステム16版の登場以降は同作のものが日本国内における 事実上の標準となり、その影響力から特に「 セガテトリス」とよく呼ばれる( 2000年にアーケードと …

まずテトリスの枠を作る

まず最初にテトリスの枠を作ります。テトリスの枠は横が12個、縦が22個のブロックでできています。なので下の図のようにそのブロック箇所へ1を立てて、ブロックを描画していけばよいことになります。

単純に書くと以下のようになりますが、それを少し整理してchar field[FIELD_HEIGHT][FIELD_WIDTH];のフィールドに値を格納する書き方に変更すると以下のようになります。

# 単純に書いた方
#include <stdio.h>
#include <memory.h>
#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
int main() {
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            if (j == 0 || j == FIELD_WIDTH - 1 || i == FIELD_HEIGHT - 1) {
                printf("■");
            } else {
                printf(" ");
            }
        }
        printf("\n");
    }
}
# 整理した方
#include 
#include 
#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
char field[FIELD_HEIGHT][FIELD_WIDTH];
int main() {
    memset(field, 0, sizeof(field));
    // 左右の壁
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        field[i][0] = 1;
        field[i][FIELD_WIDTH - 1] = 1;
    }
    // 下の壁
    for (int i = 0; i < FIELD_WIDTH; i++) {
        field[FIELD_HEIGHT - 1][i] = 1;
    }
    // 描画
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            printf(field[i][j] ? "■" : " ");
        }
        printf("\n");
    }
}

ミノを表示する

枠が表示できたら次は、ミノを表示します。ミノは下図のように7種類ありますが、ここではテトリス棒を表示することをやってみます。

枠を表示したFieldを元にし、あらたにミノを表示するdisplayBuffer領域を確保しミノを表示します。最初にミノを表示する箇所、右の黒枠箇所となります。

#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
#define MINO_TYPE_MAX 7
#define MINO_ANGLE_MAX 4
#define MINO_WIDTH 4
#define MINO_HEIGHT 4
char field[FIELD_HEIGHT][FIELD_WIDTH];
char displayBuffer[FIELD_HEIGHT][FIELD_WIDTH];
char minoShapes[MINO_TYPE_MAX][MINO_ANGLE_MAX][MINO_HEIGHT][MINO_WIDTH] = {
        // MINO_TYPE_I
        {
                // MINO_ANGLE_0
                {
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                },
         }
};
void display() {
    memcpy(displayBuffer, field, sizeof(field));
    for (int i = 0; i < MINO_HEIGHT; i++) {
        for (int j = 0; j < MINO_WIDTH; j++) {
            displayBuffer[minoY + i][minoX + j] |= minoShapes[minoType][minoAngle][i][j];
        }
    }
    system("clear");
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            printf(displayBuffer[i][j] ? "■" : " ");
        }
        printf("\n");
    }
}
int main() {
    memset(field, 0, sizeof(field));
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        field[i][0] = field[i][FIELD_WIDTH - 1] = 1;
    }
    for (int i = 0; i < FIELD_WIDTH; i++) {
        field[FIELD_HEIGHT - 1][i] = 1;
    }
    display();
}

1秒に1回更新する

以下のコードを使えば1秒に1回更新することができます。

#include 
time_t t = time(NULL);
while (true) {
    if (t != time(NULL)) {
        t = time(NULL);
        printf("%ld\n", t);
    }
}

キーボード入力を取得する

Windowsとは違いLinux環境ではkbhit()に相当するものが内容なのでこちらのサイトにある関数をそのまま利用しました。また、動画にはなかったですが、booleanを扱うために#include <stdbool.h>を追記しています。

#include 
#include 
#include 
#include 
bool kbhit() {
    struct termios oldt, newt;
    int ch;
    int oldf;
    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
    ch = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    fcntl(STDIN_FILENO, F_SETFL, oldf);
    if (ch != EOF) {
        ungetc(ch, stdin);
        return true;
    }
    return false;
}

aキーで左、sキーで下、dキーで右、スペースキーで回転するようになっています。

if (kbhit()) {
    switch (getchar()) {
        case 's':
            if (!isHit(minoX, minoY + 1, minoType, minoAngle)) {
                minoY++;
            }
            break;
        case 'a':
            if (!isHit(minoX - 1, minoY, minoType, minoAngle)) {
                minoX--;
            }
            break;
        case 'd':
            if (!isHit(minoX + 1, minoY, minoType, minoAngle)) {
                minoX++;
            }
            break;
        case 0x20:
            if (!isHit(minoX, minoY, minoType, (minoAngle + 1) % MINO_ANGLE_MAX)) {
                minoAngle = (minoAngle + 1) % MINO_ANGLE_MAX;
            }
            break;
    }
    display();
}

ミノが壁にあたるかを判定する

ミノが壁にあたるかを判定するには以下のような関数でチェックします。

bool isHit(int _minoX, int _minoY, int _minoType, int _minoAngle) {
    for (int i = 0; i < MINO_HEIGHT; i++) {
        for (int j = 0; j < MINO_WIDTH; j++) {
            if (minoShapes[_minoType][_minoAngle][i][j] && field[_minoY + i][_minoX + j]) {
                return true;
            }
        }
    }
    return false;
}

行が揃ったら消す

if (t != time(NULL)) {
    t = time(NULL);
    if (isHit(minoX, minoY + 1, minoType, minoAngle)) {
        for (int i = 0; i < MINO_HEIGHT; i++) {
            for (int j = 0; j < MINO_WIDTH; j++) {
                field[minoY + i][minoX + j] |= minoShapes[minoType][minoAngle][i][j];
            }
        }
        for (int i = 0; i < FIELD_HEIGHT - 1; i++) {
            int lineFill = 1;
            for (int j = 1; j < FIELD_WIDTH - 1; j++) {
                if (!field[i][j]) {
                    lineFill = 0;
                }
            }
            if (lineFill) {
                for (int j = i; 0 < j; j--) {
                    memcpy(field[j], field[j - 1], FIELD_WIDTH);
                }
            }
        }
        resetMino();
    } else {
        minoY++;
    }
    display();
}

まとめ

まだWindowsで実行していないのでわかりませんが、Windowsとの違いは、kbhit()#include <stdbool.h>includeするところあたりかと思います。また、Macのコンソール場合はclearコマンドで描画し直しているので履歴には残ってしまいます。動画を見て写経しただけですがテトリスってこんな感じで作れるんだなと思い楽しむことができました。改善や機能追加する点はまだたくさんあるので時間をみて更新していこうと思います。