技術文章さくさく理解できる C/C++ コンソールアプリ入門 > テトリスもどき


 

 

テトリスもどき
Nobuhide Tsuda
May-2014

概要

知らない人はめったにいないほと有名なゲーム「テトリス」の基本部分をコンソールアプリにしてみた。
テトリスは、元々はソビエト連邦の科学者アレクセイ・パジトノフ等が1984年に開発・公開したものだ。
筆者は1986年頃、会社のマックでプレイし、ハイスコアの1位~10位までを独占するまで延々やり続けていた。 周りの顰蹙をかってたような気もするが、今となってはいい思い出だ。;^p

本稿ではその「テトリス」の基本的な部分をコンソールアプリとして実装したコードを解説する。
落ち物系ゲーム実装の基本テクニックを学んで欲しい。

なお、「テトリス」は登録商標なので、テトリスを冠したゲームをおおっぴらに配布することは法律的に禁止されている。 本稿は学習目的なので問題は無いと考えているが、本稿のソースを改変し本格的なゲームにしたものを「テトリス」という名称を付けて有償・無償で配布すると、 権利者から警告を受けたり、場合によっては損害賠償の対象になる可能性があるので絶対に行わないこと。

ソースコード

遊び方

解説

画面描画まで

インクルード

#include <iostream>
#include "consoleUtil.h"

画面にテキストを表示するために iostream を、色や表示位置を指定するためなどに consoleUtil.h をインクルードしている。

consoleUtil は、コンソールに文字を出力するためのオレオレライブラリだ。
ソースは以下からDLしておくれ。

定数宣言

画面はスクリーンショットを見れば分かるようにとてもシンプルである。左側にグレイの盤面外枠がある。 落下中テトリスは青で、固定されたブロックは緑で枠内に表示される。
外枠の右側にスコアが表示される。

盤面位置などの定数は以下のように定義している。

#define     CONS_WD         80
#define     CONS_HT          25
#define     BD_WIDTH    10        // 盤面サイズ(外枠を除く)
#define     BD_HEIGHT       20
#define     N_TETRIS         7          //  テトリス種類数
#define     EMPTY              0          //  空
#define     FIXED_BLOCK    1          //  固定ブロック
#define     WALL                0xff       // 外枠
#define     TR_WIDTH        4          // 落下テトリス最大幅
#define     TR_HEIGHT       4          // 落下テトリス最大高
#define     BD_ORG_X        2          //  盤面表示位置
#define     BD_ORG_Y        1
#define     SCORE_X          (BD_ORG_X + (BD_WIDTH+2)*2 + 4)
#define     SCORE_Y          BD_ORG_Y

#define     FALL_INTERVAL     30    // 落下間隔
#define     MOVE_INTERVAL   15    //  左右移動間隔
#define     ROT_INTERVAL     15     // 回転間隔

画面位置関係以外にも、落下・左右移動・回転処理の間隔(単位は10ミリ秒)も宣言している。

グローバル変数

落下中テトリスのデータは、4x4 の2次元配列 g_tetris と、それの左上座標(g_x, g_y)で管理している。
これらは構造体にし、非グローバル変数化すべきかもしれないが、コードを単純にするため、本稿ではあえてグローバル変数とした。

下辺や他のブロックに衝突した落下中テトリスは固定ブロックとなる。盤面のどこに固定ブロックがあるかは2次元配列 g_board で管理している。
値が0(EMPTY)なら空、1(FIXED_BLOCK)なら固定ブロックありだ。

上記以外にも、落下中テトリス種別、回転番号、スコアなどの情報をグローバル変数で保持している。

intg_type;     //  テトリスタイプ
intg_x, g_y;       // 落下テトリス位置(盤面左上からの相対位置)
intg_rotIX;         // 回転番号
int g_score;       // スコア
byte g_tetris[TR_WIDTH][TR_HEIGHT];        // 落下テトリス
byte g_board[BD_WIDTH+2][BD_HEIGHT+2];  // 外枠を含めた盤面

盤面初期化

下記は、盤面を初期化するコード。

// 盤面初期化
void init_board()
{
    // 盤面の中身を全て空に
    for (int x = 1; x < BD_WIDTH + 1; ++x) {
        for (int y = 1; y < BD_HEIGHT + 1; ++y) {
            g_board[x][y] = EMPTY;     //  盤面を空に
        }
    }
    // 外枠部分に WALL を設定
    for (int x = 0; x < BD_WIDTH + 2; ++x) {
        g_board[x][0] = g_board[x][BD_HEIGHT+1] = WALL;
    }
    for (int y = 0; y < BD_HEIGHT + 2; ++y) {
        g_board[0][y] = g_board[BD_WIDTH+1][y] = WALL;
    }
}

盤面のためのグローバル変数は2次元配列で、外枠部分も含めて確保している。
余分にデータを確保するのはスネークやマインスイーパーでも出てきたテクニックで、非常によく使用する。
これにより、端かどうかのチェックが不要になり、コードがスッキリするのだ。

初期化処理は、中身を全て空にし、周りの部分に WALL を代入するだけだ。

外枠描画

// 外枠描画
void draw_frame()
{
    setColor(COL_GRAY, COL_GRAY);
    setCursorPos(BD_ORG_X, BD_ORG_Y);
    for (int x = 0; x < BD_WIDTH + 2; ++x) {
        std::cout << "  ";       // space*2
    }
    setCursorPos(BD_ORG_X, BD_ORG_Y + BD_HEIGHT + 1);
    for (int x = 0; x < BD_WIDTH + 2; ++x) {
        std::cout << "  ";       // space*2
    }
    for (int y = BD_ORG_Y+1; y < BD_ORG_Y+BD_HEIGHT+1; ++y) {
        setCursorPos(BD_ORG_X, y);
        std::cout << "  ";       // space*2
        setCursorPos(BD_ORG_X + (BD_WIDTH + 1)*2, y);
        std::cout << "  ";       // space*2
    }
}

上記は外枠の描画関数。
「setColor(COL_GRAY, COL_GRAY);」で背景色をグレイに設定し、半角空白を2個表示することで、外枠を表現している。

盤面中身表示

// 盤面描画
void draw_board()
{
    for (int y = 1; y <= BD_HEIGHT; ++y) {
        setCursorPos(BD_ORG_X + 2, y + BD_ORG_Y);
        for (int x = 1; x <= BD_WIDTH; ++x) {
            if( g_board[x][y] != EMPTY )        // 固定ブロック有り
                setColor(COL_GRAY, COL_GREEN);
            else
                setColor(COL_GRAY, COL_BLACK);
            std::cout << "  ";
        }
    }
}

上記が盤面内部の固定ブロック、ブロックが無い部分の描画。
「g_board[x][y] != EMPTY」で (x, y) に固定ブロックがあるかどうかを調べ、あれば緑で、なければ黒でひとマスを描画している。

同じ行に連続して描画する場合は、カーソル位置指定は必要ない。 行が変わった場合のみ、「setCursorPos(BD_ORG_X + 2, y + BD_ORG_Y);」を呼んで最初にカーソル位置を指定している。

スコア表示

//     スコア表示
void draw_score()
{
    setCursorPos(SCORE_X, SCORE_Y);
    setColor(COL_GRAY, COL_BLACK);
    std::cout << "SCORE:";
    std::cout.width(8);     //  表示桁数設定
    std::cout << g_score;
}

上記はスコアを表示する関数。
「setCursorPos(SCORE_X, SCORE_Y);」で指定位置にカーソルを設定し、cout で表示している。
cout.width(8) はスコアの表示幅を指定するためのもの。これが無いと桁数が減ったときに、前の文字が残る場合がある。

以上で、盤面外枠・内部、スコアの表示関数が用意できた。

演習問題:

  1. これまでのコードと以下のソースをビルドし、実行してみなさい
  2. int main()
    {
        init_board();
        draw_frame();
        draw_board();
        draw_score();
        getchar();
        return 0;
    }
    
  3. 盤面内部に適当に固定ブロックを配置し、それを表示するよう、上記コードを修正しなさい。
  4. 「000010」の様に、スコアの上位桁に 0 を表示するよう draw_score() を修正してみなさい。

落下テトリスの設定と描画

次は落下するテトリスの部分を実装しよう。

落下テトリスは一定時間ごとに1マス落下する。 これを実現するには、2次元配列の g_board[][] の該当位置データを、落下テトリス(例えば 値は2)を識別するものに設定・表示し、 一定時間ごとにそれらを1マス下にずらす、というようにするとよい。

だが、この方法は落下テトリスがどこにあるかをいちいち検索しなくてはならない。
まあ、それでも悪くはないのだが、本稿では g_tetris[][] という2次元配列にテトリスの形状情報を保持し、 g_x, g_y で位置情報を保持するというデータ構造を選んだ。 ファミコンやゲームの古い業務基板には「スプライト」という機能がハード的に用意されていた。 主人公や自機などの画像パターンデータをスプライトとして登録しておけば、 あとはスプライト表示位置を指定するだけで、画像を瞬時に移動できる。 現在であれば、画像に隠面処理をソフトで行っても処理時間が間に合わないなどということは無いのだが、 昔はCPUが貧弱だったためにそのようなハードが必要だったのだ。
本稿の落下テトリスの実装方法は、ある意味この「スプライト」と同じようなものだ。

落下テトリスパターンデータ

テトリスには7種類のパターンがある。それらが90度ごとに回転するので*4。最大は細長いテトリスなので、データは 4x4 の2次元となる。
これらを表すデータは以下のようになる。

byte trData[][4][TR_HEIGHT][TR_WIDTH] = {
    {  // I
        {
            {0, 1, 0, 0},
            {0, 1, 0, 0},
            {0, 1, 0, 0},
            {0, 1, 0, 0},
        },
        {
            {0, 0, 0, 0},
            {1, 1, 1, 1},
            {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, 0, 0, 0},
            {1, 1, 1, 1},
            {0, 0, 0, 0},
            {0, 0, 0, 0},
        },
    },
    {  // o
        .....(中略)
    }
};

全てのコードは長いので省略した。全部を見たい場合はソースコードの方を参照されたい。

落下テトリスデータの設定

void setTetris(int type, int rx)
{
    for (int y = 0; y < TR_HEIGHT; ++y) {
        for (int x = 0; x < TR_WIDTH; ++x) {
            g_tetris[x][y] = trData[type][rx][y][x];
        }
    }
}
void setTetris()
{
    g_x = (BD_WIDTH - TR_WIDTH) / 2;
    g_y = 0;
    g_type = rand() % N_TETRIS;
    setTetris(g_type, g_rotIX = 0);
}

上記は落下テトリスデータの設定関数だ。
最初のものは、タイプと回転番号(0~3)を指定して初期化するもの。
2番めのものは、固定初期位置と、タイプを乱数で設定し、1番目の関数をコールしている。

実を言うと、落下テトリスデータは、回転した場合などは総入れ替えで、個々のデータが変更されることは無い。 なので、配列にデータを保持するのではなく、データへのポインタを用意しておけば充分である。
が、それに気がついたのはプログラムを書き終えた後だった。ポインタに変えると理解が大変な人もいるかもしれないし、 いろいろ修正が大変そうだったので、ポインタへの書き換えは行わないことにした。

落下テトリスデータの表示

void draw_tetris()
{
    setColor(COL_BLUE, COL_BLUE);
    for (int i = 0; i < TR_WIDTH; ++i) {
        int y = g_y + i;
        if( y < 0 || y >= BD_HEIGHT ) continue;
        for (int k = 0; k < TR_WIDTH; ++k) {
            int x = g_x + k;
            if( x < 0 || x >= BD_WIDTH ) continue;
            if( g_tetris[k][i] ) {
                setCursorPos(BD_ORG_X + (x+1)*2, BD_ORG_Y + y + 1);
                std::cout << "  ";
            }
        }
    }
}

上記は、落下テトリスを画面に描画する関数。
落下テトリス位置(g_x, g_y)と落下テトリスデータを元に、画面に矩形文字を描いている。
範囲チェックも行っているが、これは本当は必要ない。プロラム記述開始頃は落下テトリスの移動範囲チェックを行っていなかったので、 必要だったのだが、完成した今となっては必要なくなった。
消してもいいのだが、パフォーマンスに影響なく安全な方向に冗長なのは後で仕様を変えた時に意味をもってきたりするので、 残しておいても悪くはないと考えている。

演習問題:

  1. これまでのコードと以下のソースをビルドし、実行してみなさい
  2. int main()
    {
        init_board();
        draw_frame();
        draw_board();
        draw_score();
        setTetris();
        draw_tetris();
        getchar();
        return 0;
    }
    
  3. 落下テトリス描画位置を5マス下に下げてみなさい。

落下テトリス操作処理

1ゲーム処理関数と落下処理

以上で、スコア、盤面、落下テトリスが描画出来るようになった。
次はテトリスを本当に落下させるようにしてみよう。

そのために、1ゲームの処理を行う game() 関数を導入する。

void game()
{
    g_score = 0;
    init_board();
    draw_frame();
    draw_board();
    draw_score();
    setTetris();
    draw_tetris();
    while( g_y < BD_HEIGHT ) {       // 落下テトリスが盤面下端に来るまでループ
        Sleep(100);       // 0.1秒ウェイト
        ++g_y;             // 落下テトリスを1行落下
        draw_board();
        draw_tetris();
    }
}
int main()
{
    game();
    getchar():
    return 0;
}

スネークゲームの時にも出てきたように、リアルタイムゲームは
状態を更新→画面描画→一定時間ウェイト→状態を更新・・・
を繰り返す。
本来であれば、タイマー割り込みで一定時間ごとに処理を行うのだが、コンソールアプリではそれが大変そうなので、 無限ループの途中で Sleep() を呼ぶことで時間調整を行っている。

g_y をインクリメントすることで落下テトリス位置を更新し、 draw_board(), draw_tetris() を呼ぶことで、画面を更新している。 落下テトリスのデータ構造をスプライト的にしたことで、落下処理が極めて簡単になっていることに注目して欲しい。
そして、g_y をチェックし、落下テトリスが盤面下端に達したらループを終了している。

演習問題:

  1. これまでのソースに上記コードを追加し、ビルド・実行し、テトリスがちゃんと落下するのを確認しなさい。
  2. game() ループの中の draw_board() コールは必要か?必要かどうかを考えた後に、コメントアウトしてみるとどうなるかを確認しなさい。

落下可能チェック

先のコードは落下テトリスが盤面下部に到達するとループを終了していた。
だが、それは正確な動作ではない。固定ブロックに衝突した場合もループを終了しなくてはいけない。

そのために、まずは、落下テトリスが1マス下に移動できるかどうかの判定関数を定義する。

bool can_move_down()
{
    for (int x = 0; x < TR_WIDTH; ++x) {     //  落下テトリスの最大幅について
        for (int y = TR_HEIGHT; --y >= 0; ) {
            if( g_tetris[x][y] != 0 ) {      // ブロックがある
                if( g_board[x+g_x+1][y+g_y+1+1] != EMPTY )
                    return false;              //  すぐ下に壁 or 固定ブロックがある
                break;       // この列は下に移動可能
            }
        }
    }
    return true;
}

上記がその判定関数。
落下テトリスの最大幅について、一番下にある部分を探し、そのひとつ下に対応する盤面の部分が空でなければ落下不可能なので false を返す。全ての列について落下可能であれば true を返す。

void game()
{
    .....
    for (int cnt = 1; ; ++cnt) {
        bool update = false;        // 画面更新フラグ
        if( cnt % FALL_INTERVAL == 0 )
        {      // 落下処理
            if( !can_move_down() ) {
                draw_board();
                draw_tetris();
                return;
            }
            ++g_y;     // 落下中テトリスをひとつ下に移動
            update = true;
        }
        if( update ) {
            draw_board();
            draw_tetris();
        }
        Sleep(10);
    }
}

上記が実際の落下処理の部分。
cnt % FALL_INTERVAL == 0 で落下処理をするかどうかを判定する。
落下処理をするのであれば、まず can_move_down() を呼び、1マス落下可能かどうかを判定する。
落下不可能であれば、return で game() 関数を抜ける。


落下可能であれば、g_y をインクリメントし、テトリスを1マス下に移動する。

先に説明したように、落下テトリスはスプライトのように実装しているので、移動処理時にデータを移動する必要はなく、 落下テトリスの位置を表す変数(g_x, g_y)を修正するだけで済む。便利でしょ。

update は画面更新フラグ。落下テトリスが移動した場合などは、内部データを書き変え、update を true に設定する。 ループの最後で、update フラグを調べ、立っていれば画面を再描画するというだんどりだ。

演習問題:

  1. ここまでをビルド・実行し、落下出来なくなった場合のみゲームオーバーになることを確認しなさい。

キー押下検出

次に、カーソルキーで落下テトリスを操作する処理を実装する。
が、その前にキー押下検出処理を書いておこう。

指定キーが押されているかどうかは bool isKeyPressed(int key) で判定することができる。 なので以下のように記述すれば、操作が可能ではある。

void game()
{
    ....
    for(;;) {
        if( 左矢印が押されている )
            落下テトリスを左移動;
        else if( 右矢印が押されている )
            落下テトリスを右移動;
        else if( 上矢印が押されている )
            落下テトリスを反時計回りに90度回転;
        画面描画;
        Sleep(100);   // 0.1秒ウェイト
    }
}

上記のような書き方でも、一応操作できるのだが、キーをちょんちょんと押して落下テトリスの位置・回転を微調整しようとするとうまくいかない。 なぜなら、for文の中身の処理は数ミリ秒もかからないので、ループ中のほとんどの時間は Sleep() でウェイトしている状態で、 たまたまキーを押した瞬間にキーの状態をチェックしないとそれが検出されないからだ。

この問題を避けるため、本プログラムではキーのチェックは10ミリ秒ごとに行い、落下処理はその30倍の間隔(300ミリ秒ごと)で行うようにした。

void game()
{
    .....
    int key = 0;   // 押下されたキー
    int keyDown = 0;   // 押下状態のキー
    for (int cnt = 1; ; ++cnt) {
        if( cnt % FALL_INTERVAL == 0 )
        {
            // 落下処理
            ++g_y;     // 落下中テトリスをひとつ下に移動
            update = true;
        }
        key が 0 でなければ、その処理を行う;
        if( !keyDown ) {     // キー押下を受け付けていない場合
            if( isKeyPressed(VK_LEFT) ) {
                key = keyDown = VK_LEFT;
            } else if( isKeyPressed(VK_RIGHT) ) {
                key = keyDown = VK_RIGHT;
            } else if( isKeyPressed(VK_UP) ) {
                key = keyDown = VK_UP;
            } else if( isKeyPressed(VK_DOWN) ) {
                key = keyDown = VK_DOWN;
            }
        } else {
            if( !isKeyPressed(keyDown) )// 押されたキーが離された
                keyDown = 0;
        }
        Sleep(10);     // 10ミリ秒ウェイト
    }
}

上記のコードにより、キーをちょんちょんと押しても、ちゃんと反応してくれるようになった。
反面、キーを押しっぱなしにしてもリピートが効かないようになってしまった。
リピート処理は今後の課題とする。

落下テトリスの左右移動処理

次はいよいよ、落下ブロックを左右に移動できるようにしてみよう。
ただし、無制限に左右移動してしまっては壁や固定ブロックを無視して移動できてしまう。 それではまずいので、まず移動可能かどうかをチェックする関数を用意する。

// 移動可能チェック
bool can_move_left()
{
    for (int y = 0; y < TR_HEIGHT; ++y) {
        for (int x = 0; x < TR_WIDTH; ++x) {
            if( g_tetris[x][y] != 0 ) {      // ブロックがある
                if( g_board[x+g_x+1-1][y+g_y+1] )
                    return false;              //  すぐ左に壁 or 固定ブロックがある
                break;       // この行は左に移動可能
            }
        }
    }
    return true;
}
bool can_move_right()
{
    for (int y = 0; y < TR_HEIGHT; ++y) {
        for (int x = TR_WIDTH; --x >= 0; ) {
            if( g_tetris[x][y] != 0 ) {      // ブロックがある
                if( g_board[x+g_x+1+1][y+g_y+1] )
                    return false;              //  すぐ右に壁 or 固定ブロックがある
                break;       // この行は右に移動可能
            }
        }
    }
    return true;
}

上記がその関数だ。落下テトリスの各行を左から(または右から)チェックし、最初に空でないとこを見つけたら、 その左(または右)の盤面が空かどうかをチェックする。もし空でなければ移動不可なので false を返す。
全ての行について移動可能であれば、落下テトリスは左(または右)に移動可能なので true を返す。

上記関数を使うと、移動処理は以下のように記述できる。

void game()
{
    .....
    for (int cnt = 1; ; ++cnt) {
        bool update = false;        // 画面更新フラグ
        .....
        if( cnt % MOVE_INTERVAL == 0 ) {    //  左右移動処理
            if( key == VK_LEFT ) {
                if( can_move_left() ) {
                    --g_x;
                    update = true;
                }
                key = 0;
            } else if( key == VK_RIGHT ) {
                if( can_move_right() ) {
                    ++g_x;
                    update = true;
                }
                key = 0;
            }
        }
        if( update ) {
            draw_board();
            draw_tetris();
        }
        .....
    }
}

演習問題:

  1. 左右移動処理を追加し、ビルド・実行して動作確認しなさい。

落下テトリスの回転処理

壁や固定ブロックに邪魔されて落下テトリスが回転できない場合がある。
まずはそのチェック関数の定義からだ。

// 落下テトリスが固定ブロックと重なっているか?
bool is_overlaped()
{
    for (int y = 0; y < TR_HEIGHT; ++y) {
        for (int x = 0; x < TR_WIDTH; ++x) {
            if( g_tetris[x][y] != 0 && g_board[x+g_x+1][y+g_y+1] != EMPTY )
                return true;
        }
    }
    return false;
}

上記がそのチェック関数。
実装は簡単で、落下テトリスのブロックが存在する部分について、盤面の対応するマス目が空かどうかをチェックするだけだ。 ひとつでも空でなければ false を返し、そうでなければ重なっていないので true を返す。

void game()
{
    .....
    for (int cnt = 1; ; ++cnt) {
        bool update = false;        // 画面更新フラグ
        .....
        if( cnt % ROT_INTERVAL == 0 ) {       // 回転処理
            if( key == VK_UP ) {
                int tx = g_rotIX;
                if( ++tx >= 4 ) tx = 0;
                setTetris(g_type, tx);
                if( is_overlaped() ) {       //  回転出来ない場合
                    setTetris(g_type, g_rotIX);       // 落下テトリスを元に戻す
                } else {
                    g_rotIX = tx;
                    update = true;
                }
                key = 0;
            }
        }
        if( update ) {
            draw_board();
            draw_tetris();
        }
        .....
    }
}

上記のコードは回転処理の部分。
cnt の ROT_INTERVAL 剰余を求め、それが 0 の時のみ処理を行う。
さらに、上矢印キーが押されていれば、回転処理を行う。

回転処理は、まず回転後のパターンを生成し、先に作った is_overlaped() を呼んで、重なりが無いかどうかをチェックする。
回転出来ない場合は、落下テトリスを元に戻す。
回転出来る場合は、g_rotIX を更新し、update フラグを立てて、後で画面再描画を行う。

演習問題:

  1. 回転処理を追加し、ビルド・実行して動作確認しなさい。

落下テトリスの高速落下処理

落下処理のところでは、cnt % FALL_INTERVAL == 0 の場合のみ落下処理を行っていたが、下矢印キーが押されている場合は、 無条件に落下処理を行うよう修正するとよい。
コートは下記のようになる。

void game()
{
    .....
    int key = 0;   // 押下されたキー
    int keyDown = 0;   // 押下状態のキー
    for (int cnt = 1; ; ++cnt) {
        if( cnt % FALL_INTERVAL == 0 || key == VK_DOWN )
        {
            // 落下処理
            .....
        }
        .....
        Sleep(10);     // 10ミリ秒ウェイト
    }
}

演習問題:

  1. 高速落下処理を実装し、ビルド・実行し動作確認しなさい。

その他の処理

前節までで、落下テトリスの操作が可能になった。残りは行が揃ったら消す、次のテトリス投入、再ゲーム処理くらいだ。

1行揃った場合の処理

まずは、テトリスが落下出来なくなり、固定ブロックに変換する処理。
コードは以下のようになる。

// 落下テトリスを固定ブロックに置き換える
void to_fixed()
{
    for (int y = 0; y < TR_HEIGHT; ++y) {
        for (int x = 0; x < TR_WIDTH; ++x) {
            if( g_tetris[x][y] != 0 )
                g_board[x+g_x+1][y+g_y+1] = FIXED_BLOCK;
        }
    }
}

落下テトリスがいいところに入り、1行揃った場合は、その行が消え、それより上にある固定ブロックが1行下に移動する。
下記はそのための関数だ。

void move_down(int y)
{
    while( y > 1 ) {
        for (int x = 1; x <= BD_WIDTH; ++x)
            g_board[x][y] = g_board[x][y-1];     // 1行下に移動
        --y;      //  上の行に
    }
    for (int x = 1; x <= BD_WIDTH; ++x)
        g_board[x][1] = 0;     // 最上行は空に
}

以下は揃ったラインを消去する関数。
全行について、1行に存在する固定ブロックの数をカウントしている。 ブロック数が BD_WIDTH と等しければ、1行揃ったということで、 先の move_down(y) をコールし、揃った行より上のブロックを1行下に移動する。
最後に、何行同時に消したかを見て、得点をスコアに加算している。

// 揃ったラインを消去
// 揃う可能性があるのは、落下テトリス位置だけなので、そこのみチェック
void check_line_filled()
{
    int nClear = 0;       // 消去したライン数
    for (int ty = 0; ty < TR_HEIGHT; ++ty) {
        int y = ty + g_y + 1;
        if( y > BD_HEIGHT ) break;
        int cnt = 0;
        for (int x = 1; x <= BD_WIDTH; ++x) {
            if( g_board[x][y] != EMPTY )
                ++cnt;
        }
        if( cnt == BD_WIDTH ) {// 1行揃っている場合
            move_down(y);             //  y 行を消し、固定ブロックを1行下に移動
            ++nClear;
        }
    }
    switch( nClear ) {       // 何行同時に消したか?
        case 1: g_score += 10; break;
        case 2: g_score += 40; break;
        case 3: g_score += 90; break;
        case 4: g_score += 160; break;
    }
}

下記は、テトリスが落下出来なくなった場合の処理。
to_fixed() を呼び、落下テトリスを固定ブロックに変換している。
ついで、check_line_filled() を呼び、揃った行を消去している。
あとは、スコア・盤面を再表示だ。

void game()
{
    .....
    for (int cnt = 1; ; ++cnt) {
        if( cnt % FALL_INTERVAL == 0 || key == VK_DOWN )
        {
            // 落下処理
            if( !can_move_down() ) {
                key = 0;
                to_fixed();
                check_line_filled();     //  揃ったラインを消去
                draw_score();
                draw_board();
                continue;
            }
        }
        .....
        Sleep(10);     // 10ミリ秒ウェイト
    }
}

次の落下テトリス投入

次の落下テトリスを投入するのは簡単だ。
前節のコードに数行追加するだけだ。

void game()
{
    .....
    for (int cnt = 1; ; ++cnt) {
        if( cnt % FALL_INTERVAL == 0 || key == VK_DOWN )
        {
            // 落下処理
            if( !can_move_down() ) {
                key = 0;
                to_fixed();
                check_line_filled();     //  揃ったラインを消去
                draw_score();
                setTetris();                //  新しいテトリス投入
                draw_board();
                draw_tetris();
                if( is_overlaped() )      // 新しいテトリスが固定ブロックと重なっていた場合
                    return;      // game() 関数を抜ける
                continue;
            }
        }
        .....
        Sleep(10);     // 10ミリ秒ウェイト
    }
}

落下テトリスが下辺または固定ブロックに衝突し、固定ブロックに変換したら、次の落下テトリスを投入する。
処理は簡単で、setTetris() をコールすればよい。

あと、投入したテトリスが固定ブロックと重なっていた場合は、ゲームオーバーとなるので、 is_overlaped() でチェックし、重なっていた場合は return で game() 関数を抜ける。

再ゲーム処理

最後に、1ゲームが終わった後の再ゲームの処理部分を解説する。

int main()
{
    for (;;) {
        game();    //  1ゲームの処理
        setCursorPos(0, CONS_HT - 1);
        setColor(COL_GRAY, COL_BLACK);
        std::cout << "GAME OVER. Try Again ? [y/n] ";
        for (;;) {
            if( isKeyPressed('N') )
                return 0;
            if( isKeyPressed('Y') )
                break;
            Sleep(LOOP_INTERVAL);     // 10ミリ秒ウェイト
        }
        setCursorPos(0, CONS_HT - 1);
        for (int i = 0; i < CONS_WD - 1; ++i) {
            std::cout << ' ';
        }
    }
}

コードは上記の通り。
game() から帰ってきたら、画面下部でもう一度ゲームをするかどうかを尋ね、「N」または「Y」が押されるまでウェイトする。
「N」が押された場合は、「return 0;」でプログラムを終了する。
「Y」が押された場合は、ループを抜け、画面下部のメッセージを消去して、最初に戻る。

演習問題:

  1. ハイスコアの処理を追加しなさい。

まとめ

本プログラムで使ったテクニック