70歳の挑戦 ...テトリス風GUIゲームを作ってみた

「PC98」の昔、「QuickBASIC」や「TurboC」のグラフィックスライブラリで遊んでました。Windowsになってからは中々手が出ませんでした。最近出版された、次の３冊
日経ソフトウエア 2024年1月号
日経ソフトウエア 2024年3月号

https://book.impress.co.jp/books/1122101108

のおかげで何かできそうな気が。前回の迷路ゲームに続き、今回テトリス風ゲーム作成に挑戦してみました。とは言え普段ゲームをしない人間なので、テトリスは名前を聞いたことがあるくらい。あまり詳しくは知りません。早速Web上のそれらしいゲームを少しやり、先人のコードを参考に…。しかし、残念ながら高度なコード(?)ばかりで、私には理解できません。諦めるのも癪なので、わずかな手がかりをもとに自分で組上げてみました。

![tenoros0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/c348babf-a3b3-6b77-0232-09fd8a88f55d.jpeg)
　一応、まともに動くようになったので投稿します。気付く範囲では複数のキーを押し続けるとブロックや固定物が崩れる不具合などがあります。まあ、ここまでできて自分としては満足してます。本物のテトリスを知らないヒトが作ったものなので、使い勝手の悪さなどはご容赦を。
　開発環境やDXライブラリについては、前掲の本とこのサイト
 
 https://dxlib.xsrv.jp/
 
 などを参考にしてください。長くなりますが、以下にコードのみを示します。
 （2024/04/27修正）
 ```Tenoros.cpp
#include "DxLib.h"
#include <time.h>
#include <Windows.h>

#define WIDTH 600  // ウィンドウの幅と高さのピクセル数
#define HEIGHT 480
#define Y_H 24     // フィールドの高さ（上3段と最下段の壁を含む）
#define X_W 12     // フィールドの幅（両側の壁を含む）
#define Vars 32    // ブロックの種類（回転で得られるもの全部を別種とする）
#define PI 20      // ブロック一個のピクセル数
#define Y0 1       // フィールド表示位置
#define X0 170

int field[Y_H][X_W];// ブロックなどのデータを設定
int aline[Y_H];     // 横一列の固定ブロック数
int bXL = 0, bYU = 0, bW = 0, bH = 0, bXR = 0, bYD = 0; // 現ブロックのデータの代入先
enum { TITLE, PLAY, OVER, RESET };  // 各シーンを定める定数
int scene = TITLE;                  // どのシーンの処理を行うか
int nextB5[5];

unsigned int W = GetColor(255, 255, 255);
unsigned int R = GetColor(255, 0, 0);
unsigned int G = GetColor(0, 255, 0);
unsigned int K = GetColor(0, 0, 0);
unsigned int D = GetColor(80, 80, 80);
unsigned int Y = GetColor(200, 200, 0);

int bSizeX[Vars] = { 4,1,4,1, // 各ブロックの横サイズ 
                     3,2,3,2,
                     3,2,3,2,
                     3,2,3,2,
                     3,2,3,2,
                     2,2,2,2,
                     3,2,3,2,
                     3,2,3,2
};
int bSizeY[Vars] = { 1,4,1,4, // 各ブロックの縦サイズ 
                     2,3,2,3,
                     2,3,2,3,
                     2,3,2,3,
                     2,3,2,3,
                     2,2,2,2,
                     2,3,2,3,
                     2,3,2,3
};

int blocks[Vars][4][4] = // variety, y, x
{ {
    { 1,1,1,1 },// I
    { 0,0,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 1,0,0,0 }
  },
  {
    { 1,1,1,1 },
    { 0,0,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 1,0,0,0 }
  },
  {
    { 1,1,1,0 },// 逆L
    { 0,0,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 0,1,0,0 },
    { 1,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,0,1,0 },// L
    { 1,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,0,0,0 },
    { 1,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,1,0 },
    { 1,0,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 0,1,0,0 },
    { 0,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,1,0 },// T
    { 0,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 1,1,0,0 },
    { 0,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 1,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,1,0,0 },
    { 1,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },// 逆T
    { 1,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,1,0,0 },
    { 1,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,1,0 },
    { 0,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 1,1,0,0 },
    { 0,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },// 正方形
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,1,0 },// 稲妻
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,1,0,0 },
    { 0,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,1,0 },
    { 1,1,0,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,0,0,0 },
    { 1,1,0,0 },
    { 0,1,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },// 逆稲妻
    { 0,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 1,1,0,0 },
    { 1,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 1,1,0,0 },
    { 0,1,1,0 },
    { 0,0,0,0 },
    { 0,0,0,0 }
  },
  {
    { 0,1,0,0 },
    { 1,1,0,0 },
    { 1,0,0,0 },
    { 0,0,0,0 }
  }
};

void initField() {
    for (int i = 0; i < Y_H - 1; i++) {
        for (int j = 0; j < X_W; j++) {
            if (i > 2 && (j == 0 || j == X_W - 1)) field[i][j] = 9; // 0,1,2は壁なし
            else                                   field[i][j] = 0; // 何も無い所は0（黒）
        }
        aline[i] = 0;
    }
    for (int j = 0; j < X_W; j++) field[Y_H - 1][j] = 9; // 壁は9（濃い灰色）
}

void drawField(void) {
    for (int i = 0; i < Y_H; i++) {
        for (int j = 0; j < X_W; j++) {
            if (field[i][j] == 1) // 移動中のブロックは黄
                DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, Y, TRUE);
            if (field[i][j] == 2) // 固定ブロックは白
                DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, W, TRUE);
            if (field[i][j] == 9) // 壁は濃い灰色
                DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, D, TRUE);
        }
        if (aline[i] == X_W - 3) DrawBox(X0 + PI * 0, Y0 + PI * i, X0 + PI * 0 + PI - 2, Y0 + PI * i + PI - 2, R, TRUE);
    }
}

void bRight(int n) { // 右移動
    if (bXR < X_W - 2) {
        for (int y = bH - 1; y >= 0; y--) {
            for (int x = bW - 1; x >= 0; x--)
                if (field[bYU + y][bXL + x + 1] > 1) return;  // 固定物に衝突
        }
        for (int y = bH - 1; y >= 0; y--) {
            for (int x = bW - 1; x >= 0; x--) {
                if (field[bYU + y][bXL + x] < 2 && field[bYU + y][bXL + x + 1] < 2)  // 自分自身のみ移動
                    field[bYU + y][bXL + x + 1] = field[bYU + y][bXL + x];             // フィールドデータ移動
                if (field[bYU + y][bXL + x] >= 2 && field[bYU + y][bXL + x + 1] < 2)
                    field[bYU + y][bXL + x + 1] = 0;
            }
            if (field[bYU + y][bXL] < 2) field[bYU + y][bXL] = 0;
        }
        bXL++;
        bXR++;
    }
}

void bLeft(int n) { // 左移動
    if (bXL > 1) {
        for (int y = bH - 1; y >= 0; y--) {
            for (int x = 0; x < bW; x++)
                if (field[bYU + y][bXL + x - 1] > 1 && field[bYU + y][bXL + x] == 1) return;  // 衝突
        }
        for (int y = bH - 1; y >= 0; y--) {
            for (int x = 0; x < bW; x++) {
                if (field[bYU + y][bXL + x - 1] < 2 && field[bYU + y][bXL + x] < 2)  // 自分自身のみ移動
                    field[bYU + y][bXL + x - 1] = field[bYU + y][bXL + x];
                if (field[bYU + y][bXL + x - 1] < 2 && field[bYU + y][bXL + x] >= 2)
                    field[bYU + y][bXL + x - 1] = 0;
            }
            if (field[bYU + y][bXR] < 2) field[bYU + y][bXR] = 0;
        }
        bXL--;
        bXR--;
    }
}

int rotCheck(int n0, int bXL, int bYU) {
    int l, m, n = n0;
    int bW, bH, bXR, bYD;

    l = (int)(n / 4) * 4;
    m = ((n % 4) + 1) % 4;
    n = l + m;       // 回転後のブロック番号でチェック
    bW = bSizeX[n];  // 各情報更新
    bH = bSizeY[n];
    bXR = bXL + bW - 1;
    bYD = bYU + bH - 1;
    if (bXR >= X_W - 1) {  // 右壁にぶつかったとき
        bXL -= 1;
        bXR -= 1;
    }
    if (bXL < 1) {  // 左壁にぶつかったとき
        bXL += 1;
        bXR += 1;
    }

    bool flg = true;  // Collision Check
    for (int y = 0; y < bH; y++) {
        for (int x = 0; x < bW; x++) {
            if (blocks[n][y][x] == 1 && field[bYU + y][bXL + x] > 1) {
                flg = false;  // ブロック描画位置に何かある
            }
        }
    }
    if (flg == true) return 1;
    else return 0;
}

void bRot(int* num) {  // 回転(右のみ)
    int l, m, n = *num;
    if (rotCheck(n, bXL, bYU) == 0) return;  // 回転できない

    for (int y = 0; y < bH; y++) {  // 前ブロックを消す
        for (int x = 0; x < bW; x++) {
            field[bYU + y][bXL + x] -= blocks[n][y][x];
            DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, D,TRUE);
        }
    }

    l = (int)(n / 4) * 4;
    m = ((n % 4) + 1) % 4;
    n = l + m;       // 回転後の新ブロック番号
    bW = bSizeX[n];  // 各情報更新
    bH = bSizeY[n];
    bXR = bXL + bW - 1;
    bYD = bYU + bH - 1;
    if (bXR >= X_W - 1) {  // 右壁にぶつかったとき
        bXL -= 1;
        bXR -= 1;
    }
    if (bXL < 1) {  // 左壁にぶつかったとき
        bXL += 1;
        bXR += 1;
    }

    for (int y = 0; y < bH; y++) {  // 回転後のブロック描画
        for (int x = 0; x < bW; x++) {
            if (field[bYU + y][bXL + x] <= 1) {
                if (blocks[n][y][x] == 1) {
                    field[bYU + y][bXL + x] = 1;
                    DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, Y, TRUE);
                }
                else if (blocks[n][y][x] == 0) {
                    field[bYU + y][bXL + x] = 0;
                    DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, K,TRUE);
                }
            }
        }
    }
    *num = n;
}

int rakka(int n) {
    bool descentflg = TRUE;
    bW = bSizeX[n]; bH = bSizeY[n];
    for (int x = 0; x < bW; x++) {
        for (int y = bH - 1; y > bH - 4; y--) {
            if (blocks[n][y][x] == 1 && field[bYU + y + 1][bXL + x] > 1)descentflg = FALSE; // ブロック落下不可
        }
    }
    int sum;
    if (descentflg == FALSE) {
        for (int y = 0; y < bH; y++) {
            for (int x = 0; x < bW; x++) {
                if (blocks[n][y][x] == 1) field[bYU + y][bXL + x] = 2; // ブロック固定
            }
        }

        int TG[Y_H];
        for (int i = 0; i < Y_H; i++) { TG[i] = -1; }
        sum = 0;
        for (int i = Y_H - 2; i >= 0; i--) {// 横一列揃ったか調べる
            aline[i] = 0;
            for (int j = 1; j < X_W - 1; j++) {
                if (field[i][j] == 2) aline[i] = aline[i] + 1;
            }
            if (aline[i] == X_W - 2) {      // == 10 揃った
                DrawBox(X0 + PI - 1, Y0 + PI * i - 1, X0 + PI * (X_W - 1) + 1, Y0 + PI * (i + 1), R, TRUE); // 赤で強調
                TG[sum] = i;
                sum++;
                Sleep(2);
            }
        }

        if (aline[3] > 0) return aline[3];  //--- GAME OVER ----------------

        if (sum > 0) {                      // 揃った行を消す
            for (int t = 0; t < sum; t++) {
                for (int i = TG[t] + t; i > 0; i--) {
                    DrawBox(X0 + PI - 1, Y0 + PI * i - 1, X0 + PI * (X_W - 1) + 1, Y0 + PI * i + PI + 1, R, TRUE); // 赤で強調
                    Sleep(2);
                    for (int j = 1; j < X_W - 1; j++) {
                        field[i][j] = field[i - 1][j]; // 消去行に上段のデータを移動
                        if (field[i][j] == 0)
                            DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + 18, Y0 + PI * i + 18, K, TRUE);
                        else                           // field[i][j] == 2 固定ブロックは描画
                            DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + 18, Y0 + PI * i + 18, W, TRUE);

                        Sleep(2);
                    }
                    aline[i] = aline[i - 1];           // 固定ブロック数の行データも移動
                }
                for (int j = 1; j < X_W - 1; j++) field[0][j] = 0; // 最上段は0で埋める
                Sleep(2);
            }
        }
        sum = 0;
        descentflg = TRUE;

        return -1; // 次の新ブロックの生成落下
    }
    else { // 落下可能
        bYU++; bYD++;
        for (int i = Y_H - 2; i >= 0; i--) {             // 最上段の0,1,2行もブロックを描画、落下させる
            if (i == 0) { for (int j = 1; j < X_W - 1; j++) field[i][j] = 0; } // 最上段は0で埋める
            else {
                for (int j = 1; j < X_W - 1; j++) {
                    if (field[i][j] >= 2) { ; }          // 壁、固定物があるので移動できない
                    else if (field[i - 1][j] == 2) { ; } // 固定物は移動しない
                    else {
                        field[i][j] = field[i - 1][j];   // 一行下に移動 
                        field[i - 1][j] = 0;             // 移動後は空欄
                    }
                }
            }
        }

        int tY = bYU;
        bool flg = TRUE;
        while (flg) {
            tY += 1;
            for (int y = 0; y < bH; y++) {
                for (int x = 0; x < bW; x++) {           // 現状、どこまで落下できるか調べる
                    if (field[tY + y][bXL + x] > 1 && blocks[n][y][x] == 1) {
                        flg = FALSE;
                    }
                }
            }
        }
        tY -= 1;
        for (int y = 0; y < bH; y++) {                   // 落下予想図
            for (int x = 0; x < bW; x++) {
                if (blocks[n][y][x] == 1) {
                    DrawBox(X0 + PI * (bXL + x), Y0 + PI * (tY + y), X0 + PI * (bXL + x) + 18, Y0 + PI * (tY + y) + 18, R, TRUE);
                }
            }
        }
        return 0;
    }
}

void nbDisp(int* b) {     // 次のブロック5個を表示
    int X1 = 450, X2 = 490; // 文字(番号)、ブロック位置
    for (int n = 0; n < 5; n++) {
        int i = b[n];     // numbers of next 5 blocks
        for (int y = 0; y < bSizeY[i]; y++) {
            for (int x = 0; x < bSizeX[i]; x++) {
                if (blocks[i][y][x] == 1)
                    DrawBox(X2 + PI * x, Y0 + 100 * n + PI * y, X2 + PI * x + 18, Y0 + 100 * n + PI * y + 18, Y, TRUE);
            }
        }
        SetFontSize(14);
        if (n == 0)DrawFormatString(X1, n * 100 + 5, W, "NEXT");
        else    DrawFormatString(X1 + 15, n * 100 + 5, W, "%d", n);
    }
}

void nbSet(int* num, int* b) { // 新ブロックをセット
    bW = bSizeX[*num];
    bH = bSizeY[*num];
    bYU = 4 - bH;
    int j = rand() % (X_W - bSizeX[*num] - 1) + 1;
    bXL = j;
    bXR = bXL + bW - 1;
    bYD = bYU + bH - 1;
    for (int y = 0; y < bH; y++) {
        for (int x = 0; x < bW; x++) {
            if (blocks[*num][y][x] == 1) field[bYU + y][bXL + x] = 1;
            else                         field[bYU + y][bXL + x] = 0;
        }
    }
    for (int n = 0; n < 4; n++) b[n] = b[n + 1]; // キューの更新
    b[4] = rand() % 32;
}

void gStart(int* t) {
    srand((unsigned int)time(NULL));
    initField();
    for (int i = 0; i < 5; i++) nextB5[i] = rand() % 24;  // 5個のブロック生成。初手は稲妻型以外。
    scene = PLAY;
    *t = -1; // timer = -1 で新ブロック生成、落下が始まる
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    SetWindowText("Tenoros"), SetGraphMode(WIDTH, HEIGHT, 32), ChangeWindowMode(TRUE);
    if (DxLib_Init() == -1) return -1;
    SetBackgroundColor(0, 0, 0);
    SetDrawScreen(DX_SCREEN_BACK);
    int dtimer = 5;
    int machi = 200;
    int countSP = 0;
    int bNum = 0;
    int timer = 0;
    bool jamp = FALSE;
    initField();
    for (int i = 0; i < 5; i++) nextB5[i] = rand() % 24; // 5個のブロック生成
    while (1) { // メインループ
        ClearDrawScreen();
        SetFontSize(14);
        DrawFormatString(0, 0, W, "%d", timer);
        drawField();
        switch (scene) { // 画面遷移を行うswitch case
        case TITLE:
            SetFontSize(50);
            DrawString(190, 161, "手のロス", R);
            SetFontSize(50);
            DrawString(187, 158, "手のロス", W);
            SetFontSize(20);
            DrawString(194, 240, "りたーんキーで開始", R);
            if (CheckHitKey(KEY_INPUT_RETURN)) {
                scene = PLAY;
                gStart(&timer);
            }
            break;
        case PLAY:
            SetFontSize(16);
            DrawString(5, 150, "「→」で右移動", Y);
            DrawString(5, 180, "「←」で左移動", Y);
            DrawString(5, 210, "「↓」で速く落下", Y);
            DrawString(5, 240, "「SP」で(右)回転", Y);
            DrawString(5, 320, "RキーでRESET", W);
            DrawString(5, 350, "ESCで終了", G);
            if (CheckHitKey(KEY_INPUT_R)) {
                scene = RESET;
                timer = -1;
            }
            if (CheckHitKey(KEY_INPUT_RIGHT)) bRight(bNum);
            if (CheckHitKey(KEY_INPUT_LEFT))  bLeft(bNum);
            if (CheckHitKey(KEY_INPUT_DOWN)) jamp = TRUE; // DOWNキーで落下速度アップ
            if (CheckHitKey(KEY_INPUT_SPACE)) {
                if (countSP++ > 2) { // キーのチャタリング対策
                    bRot(&bNum);
                    countSP = 0;
                }
            }
            if (timer == 0) {
                bNum = nextB5[0];
                nbSet(&bNum, nextB5);
            }
            else if (timer % dtimer == 0) {
                int t = rakka(bNum);    // ブロック落下処理
                if (t == -1) timer = -1;// 新ブロックの生成落下
                if (t > 0) scene = OVER;//if (aline[3] > 0)
            }
            nbDisp(nextB5);
            break;
        case OVER:
            SetFontSize(50);
            DrawString(173, 240, "GAME OVER", R);
            if (timer > 50) {
                scene = PLAY;
                gStart(&timer);
            }
            break;
        case RESET:
            SetFontSize(50);
            DrawString(160, 240, "GAME RESET", R);
            if (timer > 25) {
                scene = PLAY;
                gStart(&timer);
            }
            break;
        default:
            break;
        }
        timer++;
        ScreenFlip();
        if (jamp == TRUE) {
            jamp = FALSE;     // 待ち時間なし
            //dtimer = 1;
        }
        else {
            //dtimer = 5;
            WaitTimer(machi); // 一定時間待つ
        }
        if (ProcessMessage() == -1) break;
        if (CheckHitKey(KEY_INPUT_ESCAPE) == 1) break;
    }
    DxLib_End();
    return 0;
}
 ```
　最後まで御覧いただきありがとうございます。