プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第97回 プログラミングについて『テトリスゲームを作ろう! その5』
コラム
2025/01/31

第97回 プログラミングについて『テトリスゲームを作ろう! その5』

アポロレポート
今回も前回の続きです。今回はなんとか完成させるつもりです。

まず Tetris::MakeBlock メンバー関数がテスト用でしたので、本来の機能にしましょう。前回までのこの関数はテトロミノを順次表示するだけでした。

                  //前回のテスト用の部分。
  WriteBlock(1);
  Delay(0.5);
  WriteBlock(0);
  block_flag=0;

今回はこの部分を次のように書き換えます。

 if(CheckEnable(0,0,0)){
  WriteBlock(1);
 }
 else{
  playing=0;

  if(MessageBox(
     "ゲーム終了\n"
     "もう一度プレイしますか?",
     "再開",MB_YESNO)==IDYES){
   PostMessage(WM_COMMAND,ID_PLAY);
  }
  else{
   PostQuitMessage(0);
  }
 }

CheckEnable は宣言されていませんので、Tetris クラスに

int Tetris::CheckEnable(int delta_ypos,int delta_xpos,int delta_drct)

と追加しておきます。このメンバー関数は、テトロミノを回転、左右への移動、下への移動など、現在の位置からの変化量を指定することで動いても大丈夫か否かを検査する機能を持たせるものです。上の変更部分で CheckEnable(0,0,0) として検査しているのは、現在の位置(生成した最初の位置)からの変化量がすべて0ですので、現在の位置の検査を行ないます。テトロミノを新規に生成したときにそれを表示できるのであれば生成した位置(最初の位置)に表示し、そうでないときにはゲームは終了です。ゲームが終了したときには次のプレイを行なうか否かを問い合わせる様にします。

 次はその Tetris::CheckEnable メンバー関数です。

 int Tetris::CheckEnable(int delta_ypos,int delta_xpos,int delta_drct)
 {
  int tmp[5][5];
  int y,x,xx,yy;
                           //(1)
  for(y=0;y<5;y++){
   for(x=0;x<5;x++)tmp[y][x]=block_matrix[y][x];
  }
                           //(2)
  for(;delta_drct>0;delta_drct--)RotationBlock(tmp);

                           //(3)
  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(!tmp[y][x])continue;

    yy=block_ypos+y-2+delta_ypos;
    xx=block_xpos+x-2+delta_xpos;

    if(yy< 0 || yy>=GRID_SUMY)return 0;
    if(xx< 0 || xx>=GRID_SUMX)return 0;

    if(grid_matrix[yy][xx])return 0;
   }
  }

  return 1;
 }

これから回転を行なう可能性があるので、(1)では現在のテトロミノの配列を一度仮の配列にコピーしています。(2)で引数で指定された数だけ回転させます。この関数では回転は一方向だけですので、引数は0以上の数値になります(実際は0か1しか指定されません)。
 回転を行なった後は、引数で指定された左右の変化量、上下の変化量を考慮して格子の左右にはみ出さないか、また格子に既に置かれているブロックに当たらないかを検査します。大丈夫なときには1、駄目なときには0を返します。

 それではテトロミノを落下させることにします。前回までは Tetris::OnIdle のコメントにしていた部分をコメントでなくします。

 afx_msg void Tetris::OnIdle()
 {
  DWORD cur_time;

  if(!playing)return;

  cur_time=GetTickCount();
  if(cur_time-old_time>=speed){
   old_time=cur_time;

   if(block_flag==0)MakeBlock();
   else       DownBlock();   //ここのコメントをはずす。
  }
 }

DownBlock は宣言されていないので、Tetris クラスに

 void DownBlock();

を追加します。

Tetris::DownBlock は、

 int Tetris::DownBlock()
 {
  WriteBlock(0);            //落下中のテトロミノを消去。

  if(CheckEnable(1,0,0)){       //下に移動できる。
   block_ypos++;
   WriteBlock(1);
   return 1;
  }
  else{                //下に移動できない。
   block_flag=0;
   SetGridMatrix();          //現在のテトロミノの形状を格子に設定。
   old_time=0;
   return 0;
  }
 }

です。落下中のテトロミノを消去した後、下に1つ移動できるか否かを CheckEnable を使用して検査します。移動できるときには block_ypos を1つ増やしテトロミノを描画します。これで1つ下に落下したテトロミノが表示されます。

 下に移動できないときというのは、既に格子に置かれているブロックがあるか、格子の底かのどちらかです。このときには現在のテトロミノの位置の形状をそのまま格子に設定させます。SetGridMatrix も宣言されていませんので、Tetris クラスに

 void Tetris::SetGridMatrix();

を追加します。その Tetris::SetGridMatrix メンバー関数は、

 void Tetris::SetGridMatrix()
 {
  int y,x;

  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(block_matrix[y][x]){
     grid_matrix[y+block_ypos-2][x+block_xpos-2]=1;
    }
   }
  }

  Redraw();

  CheckComplete();
 }

です。格子にテトロミノの形状をセットした後は Redraw メンバー関数を使用して格子を再描画します。描画後にもう1つ行なわなければならないことがあります。それは完成した行があるときには、その行数分を点数とし、またその行は消去されなければなりません。それを行なうのが CheckComplete で、これもまだ宣言されていないので、Tetris クラスに次の様に追加します。

 void Tetris::CheckComplete();

このメンバー関数はちょっと複雑になってしまいましたが頑張って理解して下さい。

 void Tetris::CheckComplete()
 {
  int y,x,yy,flag,loop;

  for(loop=0;loop<=1;loop++){
   flag=0;
   for(y=GRID_SUMY-1;y>=0;y--){
                        //(1)
    for(x=0;x<GRID_SUMX;x++){
     if(!grid_matrix[y][x])break;
    }

    if(x<GRID_SUMX)continue;

    if(loop==0){              //(2)
     flag=1;
     for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=2;
    }
    else{                  //(3)
     for(yy=y;yy>=0;yy--){
      for(x=0;x<GRID_SUMX;x++){
       if(yy>0)grid_matrix[yy][x]=grid_matrix[yy-1][x];
       else  grid_matrix[yy][x]=0;
      }
     }

     if(++point%10==9)speed-=DELTA_SPEED;

     Redraw();

     Delay(0.5);

     y++;
    }
   }

   if(flag){                 //(4)
    Redraw();
    Delay(0.5);
   }
  }
 }

この関数は大きなループが2回繰返されます。最初のときには完成した行を表示する動作を行ないます。(1)で行が完成しているか否かを検査しています。

    for(x=0;x<GRID_SUMX;x++){
     if(!grid_matrix[y][x])break;
    }

    if(x<GRID_SUMX)continue;

行が完成していないときには上記のループをブレイクしますので、x の値は GRID_SUMXよりも必ず小さい値になっていますので、このときには次の行を検査します。行が完成しているときには、その行(格子の配列)の要素を 2 にします。2 にするのはRedraw メンバー関数が格子を再描画したときに COMPLETE 色で描画するように作成してあるためです。最初のループのときには格子全体を検査し完成してある行全部に 2 を設定した後で再描画させています。描画後にはちょっとの間(0.5秒間)そのままにしておきます。

 2回目のループのときには、完成した行を順次消去していきます。一気に消去しても構わないのですが、実際に遊んでみると何にも楽しくないので、1行ずつ消去した方がワクワクするようです。また複数行を消去するときには、格子の上の方からではなく、下の方からの方がずっと嬉しい感じがしますので、行が完成しているかどうかを検査するのは格子の下の方からにしています。
 消去は、消去する行から上の部分を1行下に詰めるようにコピーし、Redraw で再描画を行ないます。1行削除の再描画後もまたちょっとの間そのままにした方が楽しいので、Delay 関数を使用します。消去された行の数が10行毎に落下するスピード(インターバル間隔)を速くしていきます。どのくらい速くするかは好みですので色々と変えてみて下さい。

 誠に申し訳ありません。また長くなってしまいました。今回はここまでにしてこれまでの全文を紹介します。

 #include <afxwin.h>

 #define BLOCK_SIZE  8
 #define GRID_SIZE 10
 #define GRID_SUMY 22
 #define GRID_SUMX 10
 #define TOP_MARGIN  3

 #define INITIAL_SPEED 500
 #define  DELTA_SPEED 20

 #define ID_IDLE 100
 #define ID_PLAY 101

 #define FLOATING  RGB( 0, 0, 0)
 #define FIXED    RGB(127, 0, 0)
 #define BACKGROUND RGB(255,255,255)
 #define COMPLETE  RGB(255, 0, 0)

 class TetrisApp : public CWinApp
 {
  private:
   virtual BOOL OnIdle(LONG lCount);

  public:
   virtual BOOL InitInstance();
 };

 class Tetris : public CFrameWnd
 {
  private:
   int playing;
   int point ;

   DWORD old_time;
   DWORD speed;

   int  grid_matrix[GRID_SUMY][GRID_SUMX];
   int  block_matrix[5][5];
   int  block_flag,block_ypos,block_xpos;

   afx_msg void OnIdle();
   afx_msg void OnPaint();
   afx_msg void OnStartPlay();
       void Redraw();
       void WriteBlock(int wflag);
       void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
       void WritePoint();
       void MakeBlock();
       void RotationBlock(int block[5][5]);
       void Delay(double delta_time);
       int CheckEnable(int delta_ypos,int delta_xpos,int delta_drct);
       int DownBlock();
       void SetGridMatrix();
       void CheckComplete();

  public:
   Tetris();

  DECLARE_MESSAGE_MAP()
 };

 class TetrisApp TetrisApp;

 BOOL TetrisApp::InitInstance()
 {
  m_pMainWnd = new class Tetris;

  return TRUE;
 }

 BOOL TetrisApp::OnIdle(LONG lCount)
 {
  m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);

  return TRUE;
 }

 BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
  ON_COMMAND(ID_IDLE,OnIdle)
  ON_COMMAND(ID_PLAY,OnStartPlay)
  ON_WM_PAINT()
 END_MESSAGE_MAP()

 Tetris::Tetris()
 {
  RECT rect;
  int xsize,ysize;

  Create(
   NULL,"",
   WS_BORDER | WS_CAPTION | WS_OVERLAPPED | WS_MINIMIZEBOX | WS_SYSMENU,
   rectDefault);

  GetWindowRect(&rect);
  xsize=rect.right -rect.left;
  ysize=rect.bottom-rect.top ;

  GetClientRect(&rect);
  xsize-=(rect.right -rect.left);
  ysize-=(rect.bottom-rect.top );

  xsize+=GRID_SIZE* GRID_SUMX;
  ysize+=GRID_SIZE*(GRID_SUMY+TOP_MARGIN);

  SystemParametersInfo(SPI_GETWORKAREA,0,&rect,0);

  MoveWindow(
   (rect.left+rect.right )/2-xsize/2,
   (rect.top +rect.bottom)/2-ysize/2,
   xsize,ysize);

  ShowWindow(SW_SHOW);

  playing = -1;
  old_time= 0;
  speed  =INITIAL_SPEED;
 }

 afx_msg void Tetris::OnIdle()
 {
  DWORD cur_time;

  if(!playing)return;

  cur_time=GetTickCount();
  if(cur_time-old_time>=speed){
   old_time=cur_time;

   if(block_flag==0)MakeBlock();
   else       DownBlock();
  }
 }

 afx_msg void Tetris::OnPaint()
 {
  CPaintDC dc(this);

  if(playing== -1){
   playing=0;

   if(MessageBox("開始しますか?","開始",MB_YESNO)==IDNO){
     PostQuitMessage(0);
   }
   else{
    PostMessage(WM_COMMAND,ID_PLAY);
   }
  }
 }

 afx_msg void Tetris::OnStartPlay()
 {
  int y,x;

  for(y=0;y<GRID_SUMY;y++){
   for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=0;
  }

  block_flag=0;
  point   =0;
  playing  =1;
  speed   =INITIAL_SPEED;

  Redraw();
 }

 void Tetris::Redraw()
 {
  int yary[GRID_SUMX*GRID_SUMY],xary[GRID_SUMX*GRID_SUMY],lary;
  int y,x;
  int loop,color;

  for(loop=0;loop<=2;loop++){
   lary=0;
   for(y=0;y<GRID_SUMY;y++){
    for(x=0;x<GRID_SUMX;x++){
     if(grid_matrix[y][x]==loop){
      yary[lary]=y;
      xary[lary]=x;
      lary++;
     }
    }
   }

      if(loop==0)color=BACKGROUND;
   else if(loop==1)color=FIXED   ;
   else      color=COMPLETE ;
   WriteBlockTask(color,yary,xary,lary);
  }

  WritePoint();

  if(block_flag)WriteBlock(1);
 }

 void Tetris::WriteBlockTask(COLORREF color,int *yary,int *xary,int lary)
 {
  CClientDC dc(this);
  CPen  pen (PS_SOLID,0,color),*old_pen ;
  CBrush brush(      color),*old_brush;
  int i;
  int xpos,ypos;

  old_pen =dc.SelectObject(&pen );
  old_brush=dc.SelectObject(&brush);

  for(i=0;i<lary;i++){
   ypos=yary[i]*GRID_SIZE+GRID_SIZE*TOP_MARGIN+GRID_SIZE/2;
   xpos=xary[i]*GRID_SIZE           +GRID_SIZE/2;

   dc.Rectangle(
    xpos-BLOCK_SIZE/2 ,ypos-BLOCK_SIZE/2 ,
    xpos+BLOCK_SIZE/2+1,ypos+BLOCK_SIZE/2+1);
  }

  dc.SelectObject(old_pen );
  dc.SelectObject(old_brush);

   pen.DeleteObject();
  brush.DeleteObject();
 }

 void Tetris::WriteBlock(int wflag)
 {
  int yary[25],xary[25];
  int lary;
  int y,x;
  COLORREF color;

  lary=0;
  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(!block_matrix[y][x])continue;

    yary[lary]=y+block_ypos-2;
    xary[lary]=x+block_xpos-2;
    lary++;
   }
  }

  if(wflag)color=FLOATING;
  else   color=BACKGROUND;

  WriteBlockTask(color,yary,xary,lary);
 }

 void Tetris::WritePoint()
 {
  CClientDC dc(this);
  char text[21];

  if(point==0)strcpy (text,"       ");
  else    sprintf(text,"Point=%d",point);

  dc.TextOut(0,0,text,strlen(text));
 }

 void Tetris::MakeBlock()
 {
  int type;
  int y,x;

  srand(GetTickCount());

  type=rand()%7+1;

  for(y=0;y<5;y++){
   for(x=0;x<5;x++)block_matrix[y][x]=0;
  }

  if(type==1){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[2][4]=1;
  }
  else if(type==2){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
  }
  else if(type==3){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][3]=1;
  }
  else if(type==4){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
  }
  else if(type==5){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }
  else if(type==6){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
   block_matrix[3][2]=1;
  }
  else if(type==7){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }

  block_xpos=GRID_SUMX/2;
  block_ypos=2;
  block_flag=1;

  if(CheckEnable(0,0,0)){
   WriteBlock(1);
  }
  else{
   playing=0;

   if(MessageBox(
      "ゲーム終了\n"
      "もう一度プレイしますか?",
      "再開",MB_YESNO)==IDYES){
    PostMessage(WM_COMMAND,ID_PLAY);
   }
   else{
    PostQuitMessage(0);
   }
  }
 }

 void Tetris::RotationBlock(int block[5][5])
 {
  int tmp[5][5];
  int loop;
  int y,x;

  for(loop=0;loop<=1;loop++){
   for(y=0;y<5;y++){
    for(x=0;x<5;x++){
     if(loop==0) tmp[y ][x]=block[y][x];
     else    block[4-x][y]= tmp[y][x];
    }
   }
  }
 }

 void Tetris::Delay(double delta_time)
 {
  DWORD to,dt;

  dt=(int)(delta_time*1000.0);

  to=GetTickCount();

  while(1){
   if(GetTickCount()-to>dt)break;
  }
 }

 int Tetris::CheckEnable(int delta_ypos,int delta_xpos,int delta_drct)
 {
  int tmp[5][5];
  int y,x,xx,yy;

  for(y=0;y<5;y++){
   for(x=0;x<5;x++)tmp[y][x]=block_matrix[y][x];
  }

  for(;delta_drct>0;delta_drct--)RotationBlock(tmp);

  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(!tmp[y][x])continue;

    yy=block_ypos+y-2+delta_ypos;
    xx=block_xpos+x-2+delta_xpos;

    if(yy< 0 || yy>=GRID_SUMY)return 0;
    if(xx< 0 || xx>=GRID_SUMX)return 0;

    if(grid_matrix[yy][xx])return 0;
   }
  }

  return 1;
 }

 int Tetris::DownBlock()
 {
  WriteBlock(0);

  if(CheckEnable(1,0,0)){
   block_ypos++;
   WriteBlock(1);
   return 1;
  }
  else{
   block_flag=0;
   SetGridMatrix();
   old_time=0;
   return 0;
  }
 }

 void Tetris::SetGridMatrix()
 {
  int y,x;

  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(block_matrix[y][x]){
     grid_matrix[y+block_ypos-2][x+block_xpos-2]=1;
    }
   }
  }

  Redraw();

  CheckComplete();
 }

 void Tetris::CheckComplete()
 {
  int y,x,yy,flag,loop;

  for(loop=0;loop<=1;loop++){
   flag=0;
   for(y=GRID_SUMY-1;y>=0;y--){
    for(x=0;x<GRID_SUMX;x++){
     if(!grid_matrix[y][x])break;
    }

    if(x<GRID_SUMX)continue;

    if(loop==0){
     flag=1;
     for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=2;
    }
    else{
     for(yy=y;yy>=0;yy--){
      for(x=0;x<GRID_SUMX;x++){
       if(yy>0)grid_matrix[yy][x]=grid_matrix[yy-1][x];
       else  grid_matrix[yy][x]=0;
      }
     }

     if(++point%10==9)speed-=DELTA_SPEED;

     Redraw();

     Delay(0.5);

     y++;
    }
   }

   if(flag){
    Redraw();
    Delay(0.5);
   }
  }
 }

 ここまでのプログラムでとりあえずテトロミノが落下し、どんどん積もっていきます。そして最後にはゲームが終了するようになります。まだあまり楽しくはありませんが実行してみて下さい。

 それではまた次回。次回は絶対に完成させます。

そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?