LoginSignup
35
13

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

Last updated at Posted at 2024-04-18

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

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

tenoros0.jpg
 一応、まともに動くようになったので投稿します。気付く範囲では複数のキーを押し続けるとブロックや固定物が崩れる不具合などがあります。まあ、ここまでできて自分としては満足してます。本物のテトリスを知らないヒトが作ったものなので、使い勝手の悪さなどはご容赦を。
 開発環境やDXライブラリについては、前掲の本とこのサイト

などを参考にしてください。長くなりますが、以下にコードのみを示します。
(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;
}

 最後まで御覧いただきありがとうございます。

35
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
13