C++でつくったテトリスの完成品
これまではテトリミノが着地したらランダムで次のテトリミノのタイプを決めていました。しかしガイドラインではそうではなく7種類1つずつ合計7個でひとつのセット(これをバッグという)をつくり、バッグ内をランダムにシャッフルする方法をとっています。
だから同じ種類のテトリミノが3回連続して落ちてくることはありません。2回連続して同じタイプのテトリミノが落ちてきた場合は、そこにバッグの境界線があるということになります。またどのタイプのテトリミノであってもその後13回以内に同じものが落ちてくることが保証されています。
ではそのように作りかえてみましょう。
Contents
バッグをつくる
バッグをつくりましょう。これはゲームが開始されたとき、TetrisクラスのInit関数でやってしまうのがいいのではないでしょうか?
vectorクラスで対応
まずヘッダーファイルTetris.hに
1 |
vector<TetriminoTypes> m_vectorTetriminoTypes; |
と書きたいのですが、この場合はTetris.hをincludeするcppファイルであれば先にvectorをincludeしなければなりません。
1 2 |
#include <vector> using namespace std; |
を書かないといけません。
これ一行includeするだけでOKというall.hを作りましたが、そこに
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#pragma once #include <windows.h> // <vector>はTetris.hよりも先にincludeする #include <vector> using namespace std; #include "Block.h" #include "Tetris.h" #include "MainWindow.h" // それ以外の部分は省略 |
とやっておけばよいでしょう。
バッグをつくるために必要な変数と関数
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Tetris { private: // Init関数が長くなるのでm_FixedBlocks初期化の処理は独立させた void InitFixedBlocks(); // バッグをつくるために必要な変数と関数 private: vector<TetriminoTypes> m_vectorTetriminoTypes; void CreateBag(); // その他の既出のメンバ変数とメンバ関数は省略する }; |
ゲームの初期化の処理をする関数です。InitFixedBlocksが長いのでInit関数全体が長くならないように独立させました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void Tetris::Init() { InitFixedBlocks(); for (int i = 0; i < 4; i++) m_DeleteLineNumbers[i] = -1; unsigned int now = (unsigned int)time(0); srand(now); CreateBag(); NewTetrimino(); } void Tetris::InitFixedBlocks() { for (int row = 0; row < 20; row++) { for (int column = 0; column < 10; column++) { delete m_FixedBlocks[row][column]; m_FixedBlocks[row][column] = NULL; } } } |
Init関数内で呼び出しているCreateBag関数とNewTetrimino関数ですが、以下のようになっています。
CreateBag関数は7種類1個ずつを集めたもののなかからランダムでひとつずつ選んだものをm_vectorTetriminoTypesに格納していきます。
ストックが7個以下になったら新しいバッグをつくる
次に出現するテトリミノのタイプはm_vectorTetriminoTypesから最初の要素が取り出されてなくなっていきます。残りが7個以下になったら新しいバッグをつくってm_vectorTetriminoTypesに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void Tetris::CreateBag() { if(m_vectorTetriminoTypes.size() > 7) return; vector<TetriminoTypes> v; v.push_back(TetriminoTypes::I); v.push_back(TetriminoTypes::J); v.push_back(TetriminoTypes::L); v.push_back(TetriminoTypes::O); v.push_back(TetriminoTypes::S); v.push_back(TetriminoTypes::T); v.push_back(TetriminoTypes::Z); while(true) { int size = v.size(); if(size == 0) break; int type = rand() % size; TetriminoTypes selectedType = v[type]; m_vectorTetriminoTypes.push_back(selectedType); v.erase(v.begin() + type); } } |
Init関数内でNewTetrimino関数を呼び出しています。こうすることでゲームの最初に現れるテトロミノも固定されたものではなくランダムになります。
1 2 3 4 5 6 7 |
void Tetris::NewTetrimino() { m_TetriminoAngle = TetriminoAngle::Angle0; m_TetriminoPositionX = 0; m_TetriminoPositionY = 0; SetNewBlockType(); } |
SetNewBlockType関数はInit関数によって呼ばれるだけでなく、テトリミノが着地したあとでも呼び出されます。次に降らせるテトリミノをm_CurTetriminoTypeにセットする関数です。m_vectorTetriminoTypesから先頭のものを取り出して使います。
1 2 3 4 5 6 7 8 9 10 11 12 |
void Tetris::SetNewBlockType() { m_CurTetriminoType = m_vectorTetriminoTypes[0]; m_vectorTetriminoTypes.erase(m_vectorTetriminoTypes.begin()); // m_vectorTetriminoTypesの要素数が不足していたら補充する CreateBag(); // 次に落ちてくるテトリミノを表示するときに必要 // m_CurTetriminoTypeの中身が変更になったタイミングでウィンドウ全体を再描画する InvalidateRect(hMainWnd, NULL, TRUE); } |
これで同じ種類のテトリミノが3個以上連続で落ちてくることはなくなりました。
次に落ちてくるテトリミノをいくつか表示させる
次の課題ですが、次に落ちてくるNextブロックを右側に表示させます。4つくらい表示させればよいのではないかと考えています。
ヘッダーファイルに追加する関数を記述します。
1 2 3 4 5 6 7 8 9 |
class Tetris { // NEXTブロックの表示をするための関数 private: void ShowNext(HDC hdc); void DrawNextBlock(HDC hdc, POINT leftTpo, TetriminoTypes tetriminoType); void GetColumnsRowsNextBlock(TetriminoTypes tetriminoType, int columns[], int rows[], int size); Color GetTetriminoColor(TetriminoTypes tetriminoType); }; |
ShowNext関数
まず次に落ちてくるNextブロックを描画するための関数をつくり、Tetris::Draw関数に追加します。
1 2 3 4 5 6 7 8 9 10 11 |
void Tetris::Draw(HDC hdc) { DrawGhostBlocks(hdc); DrawFixedBlock(hdc); DrawOutsideBlocks(hdc); DrawTetrimino(hdc); ShowScore(hdc); ShowNext(hdc); } |
ShowNext関数は一番上に「NEXT」と表示し、次の4つ分のテトリミノを表示させる関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void Tetris::ShowNext(HDC hdc) { // 文字列「NEXT」を表示 SetTextColor(hdc, RGB(255, 255, 255)); HFONT hFont = SetScoreFont(hdc, 16); HFONT hFontOld = (HFONT)SelectObject(hdc, hFont); const WCHAR *str = L"NEXT"; TextOut(hdc, 400, 50, str, lstrlen(str)); SelectObject(hdc, hFontOld); DeleteObject(hFont); // テトリミノを表示する場所を決める // いまのところ(400, 100)、あとはY座標を80ずつ増やしていくのがよさそう POINT leftTop; leftTop.x = 400; leftTop.y = 100; DrawNextBlock(hdc, leftTop, m_vectorTetriminoTypes[0]); leftTop.y = 180; DrawNextBlock(hdc, leftTop, m_vectorTetriminoTypes[1]); leftTop.y = 260; DrawNextBlock(hdc, leftTop, m_vectorTetriminoTypes[2]); leftTop.y = 340; DrawNextBlock(hdc, leftTop, m_vectorTetriminoTypes[3]); } |
DrawNextBlock関数
DrawNextBlock関数は左上の座標とテトリミノの種類を指定すればその位置にテトリミノを表示させる関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void Tetris::DrawNextBlock(HDC hdc, POINT leftTpo, TetriminoTypes tetriminoType) { COLORREF colorref = GetColor(GetTetriminoColor(tetriminoType)); HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0)); HPEN hOldPen = (HPEN)SelectObject(hdc, hPen); HBRUSH hBrush = CreateSolidBrush(colorref); HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); int columns[4]; int rows[4]; GetColumnsRowsNextBlock(tetriminoType, columns, rows, 4); int blockSize = 16; // フィールドに表示されているもの(=20)よりやや小さめのサイズに for (int i = 0; i < 4; i++) { int x = columns[i] * blockSize + leftTpo.x; int y = rows[i] * blockSize + leftTpo.y; ::Rectangle(hdc, x, y, x + blockSize, y + blockSize); } SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); DeleteObject(hPen); DeleteObject(hBrush); } |
GetColumnsRowsNextBlock関数
表示させるテトリミノはブロックが集まって構成されていますが、そのそれぞれのブロックを4×4のどの部分に表示させればいいのでしょうか? これを取得するのがGetColumnsRowsNextBlock関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
void Tetris::GetColumnsRowsNextBlock(TetriminoTypes tetriminoType, int columns[], int rows[], int size) { if(size != 4) return; if (tetriminoType == TetriminoTypes::I) { columns[0] = 0; columns[1] = 1; columns[2] = 2; columns[3] = 3; rows[0] = 1; rows[1] = 1; rows[2] = 1; rows[3] = 1; return; } if (tetriminoType == TetriminoTypes::J) { columns[0] = 0; columns[1] = 0; columns[2] = 1; columns[3] = 2; rows[0] = 0; rows[1] = 1; rows[2] = 1; rows[3] = 1; return; } if (tetriminoType == TetriminoTypes::L) { columns[0] = 0; columns[1] = 1; columns[2] = 2; columns[3] = 2; rows[0] = 1; rows[1] = 1; rows[2] = 1; rows[3] = 0; return; } if (tetriminoType == TetriminoTypes::O) { columns[0] = 0; columns[1] = 0; columns[2] = 1; columns[3] = 1; rows[0] = 0; rows[1] = 1; rows[2] = 0; rows[3] = 1; return; } if (tetriminoType == TetriminoTypes::S) { columns[0] = 0; columns[1] = 1; columns[2] = 1; columns[3] = 2; rows[0] = 1; rows[1] = 0; rows[2] = 1; rows[3] = 0; return; } if (tetriminoType == TetriminoTypes::T) { columns[0] = 0; columns[1] = 1; columns[2] = 1; columns[3] = 2; rows[0] = 1; rows[1] = 0; rows[2] = 1; rows[3] = 1; return; } if (tetriminoType == TetriminoTypes::Z) { columns[0] = 0; columns[1] = 1; columns[2] = 1; columns[3] = 2; rows[0] = 0; rows[1] = 0; rows[2] = 1; rows[3] = 1; return; } } |
GetTetriminoColor関数
最後にテトリミノの色を取得しなければなりません。引数なしのGetTetriminoColor関数は現在落下中のテトロミノの色を取得することができますが、いまはこの関数は使えないので引数付きの似たような関数をつくりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Color Tetris::GetTetriminoColor(TetriminoTypes tetriminoType) { if (tetriminoType == TetriminoTypes::I) return Color::Aqua; else if (tetriminoType == TetriminoTypes::J) return Color::Blue; else if (tetriminoType == TetriminoTypes::L) return Color::Orange; else if (tetriminoType == TetriminoTypes::O) return Color::Yellow; else if (tetriminoType == TetriminoTypes::S) return Color::Green; else if (tetriminoType == TetriminoTypes::T) return Color::Violet; else if (tetriminoType == TetriminoTypes::Z) return Color::Red; else return Color::Gray; } |
テトリミノの初期座標の変更とこれにともなう変更
それから新しく落ちてくるテトロミノの初期位置ですが、いまは(0, 0)の位置にしていますが、普通のテトリスではX軸では真ん中あたり、Y軸ではもっと上になっています。
1 2 3 4 5 6 7 |
void Tetris::NewTetrimino() { m_TetriminoAngle = TetriminoAngle::Angle0; m_TetriminoPositionX = 3; m_TetriminoPositionY = -1; SetNewBlockType(); } |
しかしこれではバグります。移動できるか回転できるかを判定するときにm_FixedBlocksのブロックがある部分と衝突していないかを調べているのですが、このとき配列の添え字に負数を指定するとエラーが発生します。
CanMove関数とCanRotate関数の変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
BOOL Tetris::CanMove(int x, int y) { TetriminoPosition pos[4]; GetTetriminosPosition(pos, 4); for (int i = 0; i < 4; i++) { if (pos[i].m_column + x > COLUMN_MAX - 1) return FALSE; if (pos[i].m_column + x < 0) return FALSE; if (pos[i].m_row + y > ROW_MAX - 1) return FALSE; if(pos[i].m_row + y < 0) // この場合は評価しない continue; if(m_FixedBlocks[pos[i].m_row + y][pos[i].m_column + x] != NULL) return FALSE; } return TRUE; } |
Tetris::CanRotate(BOOL isRight, int x, int y)関数でも似たようなところがあるので同じようにしてエラーを回避します。
フィールドよりも上にあるテトリミノは描画しない
もうひとつの問題はフィールドよりも上の部分は特別な場合を除き再描画の対象となっていません。対象に含めるために関数を書き直してもいいのですが、ブロックのうち、pos[i].m_rowが負数の場合はそもそも描画しないという方法で対応します。
1 2 3 4 5 6 7 8 |
void Tetris::DrawBlock(HDC hdc, int column, int row, Color color) { if(row < 0) return; // rowが負数なら描画のための処理はしない } |
それから着地したときにテトリミノを固定する処理でもyが負数のときにm_FixedBlocks[y][x]にpBlockをセットするとバグがおきます。そこで配列 m_FixedBlocksにpBlockをセットするときに確認の処理を追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void Tetris::FixBlocks() { TetriminoPosition blocks[4]; GetTetriminosPosition(blocks, 4); for (int i = 0; i < 4; i++) { Block* pBlock = new Block(); pBlock->color = GetTetriminoColor(); if(blocks[i].m_row >= 0) m_FixedBlocks[blocks[i].m_row][blocks[i].m_column] = pBlock; } } |
あとはホールドの機能ですが、これは次回、ホールドの機能を実装する 猫並みの知能を得るためにC++でテトリスをつくる(6)でおこないます。