【第一回】C#でテトリス作成

【第一回】C#でテトリス作成

目次

はじめに
MVCモデルとは
プロジェクト作成
要件定義
実装 Model編
ゲーム盤 GameBoardクラス実装
ゲーム全体の管理 TetrisGameクラス実装
まとめ

はじめに

引続きC#の学習の為にアプリを作成してみます。
今回はテトリスを作成してみたいと思います。
以前、テトリスを作成したことがあるのですが、その際は参考にしていたサイトが途中までしか更新されず
一人で続きを作成する知識もなかったので頓挫してしまいました。
Claudeを使用しつつ改めて最後まで作成しきる様子を執筆していきます。

MVCモデルとは

簡単にMVCモデルとは何かを説明します。
Model・・・データとビジネスロジックを管理
View・・・UIを担当
Controller・・・ModelとViewの仲介役
上記の頭文字をとってMVCと呼ばれるものです。
アプリの構造を3つに分離してそれぞれに役割を持たせることにより、コードが整理され理解しやすくなったり
各構造が独立しているため、機能の追加・修正がやりやすくなったりというメリットがあります。

プロジェクト作成

いつも通りVisual Studioにてプロジェクト作成から始めていきます。
今回はテトリスを作成するのでMVCモデルと呼ばれる形で作成していきます。
まずVisual Studioを起動して「新しいプロジェクトの作成」をクリック
テンプレートはASP.NET Core Webアプリにてプロジェクト作成から始めていきます。
今回はテトリスを作成するのでMVCモデルと呼ばれる形で作成していきます。
まずVisual Studioを起動して「新しいプロジェクトの作成」をクリック。
テンプレートはASP.NET Core Webアプリ(Model-View-Controller)を選択します。
プロジェクト名を入力して作成
作成するとソリューションエクスプローラーと呼ばれる場所にMVCでフォルダが生成されます。

要件定義

少し順番は前後してしまいましたが要件定義を簡単にしておこうと思います。
・盤面サイズ:横10マス×縦20マス
・テトリミノ:7種類(I、O、T、S、Z、J、L)
・ソフトドロップ・ハードドロップ
・スコアシステム
・レベルアップシステム
以上の機能を簡単に要件定義として設定します。

実装 Model編

では早速コードを書き始めていきたいと思います。
まずはModelsフォルダに、テトリミノ(落ちてくるブロック)のクラスを作っていきます。
フォルダ内で新しくクラスを作成します。
クラス名はTetrominoとし、下記のコードを書きます。

<code>namespace TetrisGame.Models
{
    /// &lt;summary&gt;
    /// テトロミノ(テトリスのブロック)を表すクラス
    /// &lt;/summary&gt;
    public class Tetromino
    {
        /// &lt;summary&gt;
        /// テトロミノの種類
        /// &lt;/summary&gt;
        public enum TetrominoType
        {
            I, // 直線型
            O, // 正方型  
            T, // T字型
            S, // S字型
            Z, // Z字型
            J, // J字型
            L  // L字型
        }

        /// &lt;summary&gt;
        /// テトロミノの種類
        /// &lt;/summary&gt;
        public TetrominoType Type { get; set; }

        /// &lt;summary&gt;
        /// 現在のX座標(ゲーム盤上の位置)
        /// &lt;/summary&gt;
        public int X { get; set; }

        /// &lt;summary&gt;
        /// 現在のY座標(ゲーム盤上の位置)
        /// &lt;/summary&gt;
        public int Y { get; set; }

        /// &lt;summary&gt;
        /// 回転状態(0-3: 0度、90度、180度、270度)
        /// &lt;/summary&gt;
        public int Rotation { get; set; }

        /// &lt;summary&gt;
        /// テトロミノの形状データ(4x4の配列)
        /// &lt;/summary&gt;
        public bool&#91;,] Shape { get; private set; }

        /// &lt;summary&gt;
        /// コンストラクタ
        /// &lt;/summary&gt;
        /// &lt;param name="type"&gt;テトロミノの種類&lt;/param&gt;
        public Tetromino(TetrominoType type)
        {
            Type = type;
            X = 4; // ゲーム盤の中央あたりから開始
            Y = 0; // 一番上から開始
            Rotation = 0; // 回転なし
            Shape = GetInitialShape(type);
        }

        /// &lt;summary&gt;
        /// テトロミノの種類に応じた初期形状を取得
        /// &lt;/summary&gt;
        /// &lt;param name="type"&gt;テトロミノの種類&lt;/param&gt;
        /// &lt;returns&gt;4x4の形状配列&lt;/returns&gt;
        private bool&#91;,] GetInitialShape(TetrominoType type)
        {
            // 4x4の配列を初期化(全てfalse)
            bool&#91;,] shape = new bool&#91;4, 4];

            switch (type)
            {
                case TetrominoType.I: // 直線型 ████
                    shape&#91;1, 0] = shape&#91;1, 1] = shape&#91;1, 2] = shape&#91;1, 3] = true;
                    break;

                case TetrominoType.O: // 正方型 ██
                    shape&#91;1, 1] = shape&#91;1, 2] = shape&#91;2, 1] = shape&#91;2, 2] = true; //      ██
                    break;

                case TetrominoType.T: // T字型 ███
                    shape&#91;1, 1] = shape&#91;1, 2] = shape&#91;1, 3] = shape&#91;2, 2] = true; //       █
                    break;

                case TetrominoType.S: // S字型  ██
                    shape&#91;1, 1] = shape&#91;1, 2] = shape&#91;2, 0] = shape&#91;2, 1] = true; //      ██
                    break;

                case TetrominoType.Z: // Z字型 ██
                    shape&#91;1, 0] = shape&#91;1, 1] = shape&#91;2, 1] = shape&#91;2, 2] = true; //       ██
                    break;

                case TetrominoType.J: // J字型 █
                    shape&#91;1, 0] = shape&#91;2, 0] = shape&#91;2, 1] = shape&#91;2, 2] = true; //      ███
                    break;

                case TetrominoType.L: // L字型   █
                    shape&#91;1, 2] = shape&#91;2, 0] = shape&#91;2, 1] = shape&#91;2, 2] = true; //      ███
                    break;
            }

            return shape;
        }

        /// &lt;summary&gt;
        /// テトロミノを右に90度回転
        /// &lt;/summary&gt;
        public void RotateClockwise()
        {
            Rotation = (Rotation + 1) % 4;
            Shape = RotateShapeClockwise(Shape);
        }

        /// &lt;summary&gt;
        /// 4x4配列を時計回りに90度回転
        /// &lt;/summary&gt;
        /// &lt;param name="original"&gt;元の配列&lt;/param&gt;
        /// &lt;returns&gt;回転後の配列&lt;/returns&gt;
        private bool&#91;,] RotateShapeClockwise(bool&#91;,] original)
        {
            bool&#91;,] rotated = new bool&#91;4, 4];
            
            for (int i = 0; i &lt; 4; i++)
            {
                for (int j = 0; j &lt; 4; j++)
                {
                    rotated&#91;j, 3 - i] = original&#91;i, j];
                }
            }
            
            return rotated;
        }

        /// &lt;summary&gt;
        /// テトロミノを左に移動
        /// &lt;/summary&gt;
        public void MoveLeft()
        {
            X--;
        }

        /// &lt;summary&gt;
        /// テトロミノを右に移動  
        /// &lt;/summary&gt;
        public void MoveRight()
        {
            X++;
        }

        /// &lt;summary&gt;
        /// テトロミノを下に移動
        /// &lt;/summary&gt;
        public void MoveDown()
        {
            Y++;
        }
    }
}</code>

ゲーム盤 GameBoardクラス実装

続けてゲーム盤の実装をします。
新しくGameBoardクラスを作成し、以下のコードを書きます。
こちらはゲーム盤(プレイエリア)での設定部分を実装しています。

<code>namespace TetrisGame.Models
{
    /// &lt;summary&gt;
    /// テトリスのゲーム盤を管理するクラス
    /// &lt;/summary&gt;
    public class GameBoard
    {
        /// &lt;summary&gt;
        /// ゲーム盤の幅(マス数)
        /// &lt;/summary&gt;
        public const int Width = 10;

        /// &lt;summary&gt;
        /// ゲーム盤の高さ(マス数)  
        /// &lt;/summary&gt;
        public const int Height = 20;

        /// &lt;summary&gt;
        /// ゲーム盤の状態(0=空、1以上=ブロックあり)
        /// 数値はテトロミノの種類を表す
        /// &lt;/summary&gt;
        public int&#91;,] Board { get; private set; }

        /// &lt;summary&gt;
        /// コンストラクタ - 空のゲーム盤を初期化
        /// &lt;/summary&gt;
        public GameBoard()
        {
            Board = new int&#91;Height, Width];
            ClearBoard();
        }

        /// &lt;summary&gt;
        /// ゲーム盤をクリア(全て0にする)
        /// &lt;/summary&gt;
        public void ClearBoard()
        {
            for (int row = 0; row &lt; Height; row++)
            {
                for (int col = 0; col &lt; Width; col++)
                {
                    Board&#91;row, col] = 0;
                }
            }
        }

        /// &lt;summary&gt;
        /// テトロミノが指定位置に配置可能かチェック
        /// &lt;/summary&gt;
        /// &lt;param name="tetromino"&gt;チェックするテトロミノ&lt;/param&gt;
        /// &lt;param name="x"&gt;X座標&lt;/param&gt;
        /// &lt;param name="y"&gt;Y座標&lt;/param&gt;
        /// &lt;returns&gt;配置可能ならtrue&lt;/returns&gt;
        public bool CanPlaceTetromino(Tetromino tetromino, int x, int y)
        {
            for (int row = 0; row &lt; 4; row++)
            {
                for (int col = 0; col &lt; 4; col++)
                {
                    // テトロミノのこの位置にブロックがない場合はスキップ
                    if (!tetromino.Shape&#91;row, col])
                        continue;

                    // ゲーム盤上の実際の座標を計算
                    int boardX = x + col;
                    int boardY = y + row;

                    // 範囲外チェック
                    if (boardX &lt; 0 || boardX &gt;= Width || boardY &gt;= Height)
                        return false;

                    // 上端は許可(テトロミノが画面上から入ってくる)
                    if (boardY &lt; 0)
                        continue;

                    // 既存のブロックとの衝突チェック
                    if (Board&#91;boardY, boardX] != 0)
                        return false;
                }
            }

            return true;
        }

        /// &lt;summary&gt;
        /// テトロミノを ゲーム盤に固定する
        /// &lt;/summary&gt;
        /// &lt;param name="tetromino"&gt;固定するテトロミノ&lt;/param&gt;
        public void PlaceTetromino(Tetromino tetromino)
        {
            for (int row = 0; row &lt; 4; row++)
            {
                for (int col = 0; col &lt; 4; col++)
                {
                    if (tetromino.Shape&#91;row, col])
                    {
                        int boardX = tetromino.X + col;
                        int boardY = tetromino.Y + row;

                        // 範囲内かつ有効な位置の場合のみ配置
                        if (boardX &gt;= 0 &amp;&amp; boardX &lt; Width &amp;&amp; 
                            boardY &gt;= 0 &amp;&amp; boardY &lt; Height)
                        {
                            // テトロミノの種類+1を保存(0は空きマス用)
                            Board&#91;boardY, boardX] = (int)tetromino.Type + 1;
                        }
                    }
                }
            }
        }

        /// &lt;summary&gt;
        /// 完成した行を見つけて削除し、削除行数を返す
        /// &lt;/summary&gt;
        /// &lt;returns&gt;削除した行数&lt;/returns&gt;
        public int ClearCompletedLines()
        {
            List&lt;int&gt; completedLines = new List&lt;int&gt;();

            // 完成した行を探す
            for (int row = 0; row &lt; Height; row++)
            {
                bool isLineComplete = true;
                for (int col = 0; col &lt; Width; col++)
                {
                    if (Board&#91;row, col] == 0)
                    {
                        isLineComplete = false;
                        break;
                    }
                }

                if (isLineComplete)
                {
                    completedLines.Add(row);
                }
            }

            // 完成した行を削除
            foreach (int lineIndex in completedLines)
            {
                RemoveLine(lineIndex);
            }

            return completedLines.Count;
        }

        /// &lt;summary&gt;
        /// 指定した行を削除し、上の行を下に移動
        /// &lt;/summary&gt;
        /// &lt;param name="lineIndex"&gt;削除する行のインデックス&lt;/param&gt;
        private void RemoveLine(int lineIndex)
        {
            // 削除する行より上の行を1つずつ下に移動
            for (int row = lineIndex; row &gt; 0; row--)
            {
                for (int col = 0; col &lt; Width; col++)
                {
                    Board&#91;row, col] = Board&#91;row - 1, col];
                }
            }

            // 一番上の行をクリア
            for (int col = 0; col &lt; Width; col++)
            {
                Board&#91;0, col] = 0;
            }
        }

        /// &lt;summary&gt;
        /// ゲームオーバー判定(一番上の行にブロックがある)
        /// &lt;/summary&gt;
        /// &lt;returns&gt;ゲームオーバーならtrue&lt;/returns&gt;
        public bool IsGameOver()
        {
            for (int col = 0; col &lt; Width; col++)
            {
                if (Board&#91;0, col] != 0)
                    return true;
            }
            return false;
        }

        /// &lt;summary&gt;
        /// ゲーム盤の状態をコンソールに出力(デバッグ用)
        /// &lt;/summary&gt;
        public void PrintBoard()
        {
            Console.WriteLine("Current Board State:");
            for (int row = 0; row &lt; Height; row++)
            {
                for (int col = 0; col &lt; Width; col++)
                {
                    Console.Write(Board&#91;row, col] == 0 ? "." : "#");
                }
                Console.WriteLine();
            }
            Console.WriteLine();
        }
    }
}</code>

ゲーム全体の管理 TetrisGameクラス実装

次にゲーム全体の管理を行うTetrisGameクラスを実装します。
新しくTetrisGameクラスを作成し、以下のコードを書きます。

<code>namespace TetrisGame.Models
{
    /// &lt;summary&gt;
    /// テトリスゲーム全体を管理するクラス
    /// &lt;/summary&gt;
    public class TetrisGame
    {
        /// &lt;summary&gt;
        /// ゲーム盤
        /// &lt;/summary&gt;
        public GameBoard Board { get; private set; }

        /// &lt;summary&gt;
        /// 現在落下中のテトロミノ
        /// &lt;/summary&gt;
        public Tetromino? CurrentPiece { get; private set; }

        /// &lt;summary&gt;
        /// 次に出現するテトロミノ
        /// &lt;/summary&gt;
        public Tetromino? NextPiece { get; private set; }

        /// &lt;summary&gt;
        /// 現在のスコア
        /// &lt;/summary&gt;
        public int Score { get; private set; }

        /// &lt;summary&gt;
        /// 消去した行数
        /// &lt;/summary&gt;
        public int Lines { get; private set; }

        /// &lt;summary&gt;
        /// 現在のレベル
        /// &lt;/summary&gt;
        public int Level { get; private set; }

        /// &lt;summary&gt;
        /// ゲーム終了フラグ
        /// &lt;/summary&gt;
        public bool IsGameOver { get; private set; }

        /// &lt;summary&gt;
        /// ゲーム一時停止フラグ
        /// &lt;/summary&gt;
        public bool IsPaused { get; set; }

        /// &lt;summary&gt;
        /// テトロミノ生成用の乱数
        /// &lt;/summary&gt;
        private Random _random;

        /// &lt;summary&gt;
        /// 最後にテトロミノが自動落下した時刻
        /// &lt;/summary&gt;
        public DateTime LastDropTime { get; set; }

        /// &lt;summary&gt;
        /// 落下間隔(ミリ秒)
        /// &lt;/summary&gt;
        public int DropInterval =&gt; Math.Max(50, 1000 - Level * 50);

        /// &lt;summary&gt;
        /// コンストラクタ
        /// &lt;/summary&gt;
        public TetrisGame()
        {
            Board = new GameBoard();
            Score = 0;
            Lines = 0;
            Level = 1;
            IsGameOver = false;
            IsPaused = false;
            _random = new Random();
            LastDropTime = DateTime.Now;
            
            // 最初のテトロミノを生成
            NextPiece = GenerateRandomTetromino();
            SpawnNextPiece();
        }

        /// &lt;summary&gt;
        /// ランダムなテトロミノを生成
        /// &lt;/summary&gt;
        /// &lt;returns&gt;新しいテトロミノ&lt;/returns&gt;
        private Tetromino GenerateRandomTetromino()
        {
            var types = Enum.GetValues&lt;Tetromino.TetrominoType&gt;();
            var randomType = types&#91;_random.Next(types.Length)];
            return new Tetromino(randomType);
        }

        /// &lt;summary&gt;
        /// 次のテトロミノをゲーム盤に出現させる
        /// &lt;/summary&gt;
        private void SpawnNextPiece()
        {
            CurrentPiece = NextPiece;
            NextPiece = GenerateRandomTetromino();

            // 新しいピースが配置できない場合はゲームオーバー
            if (CurrentPiece != null &amp;&amp; !Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X, CurrentPiece.Y))
            {
                IsGameOver = true;
            }
        }

        /// &lt;summary&gt;
        /// テトロミノを左に移動
        /// &lt;/summary&gt;
        /// &lt;returns&gt;移動できた場合true&lt;/returns&gt;
        public bool MoveLeft()
        {
            if (CurrentPiece == null || IsGameOver || IsPaused)
                return false;

            if (Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X - 1, CurrentPiece.Y))
            {
                CurrentPiece.MoveLeft();
                return true;
            }
            return false;
        }

        /// &lt;summary&gt;
        /// テトロミノを右に移動
        /// &lt;/summary&gt;
        /// &lt;returns&gt;移動できた場合true&lt;/returns&gt;
        public bool MoveRight()
        {
            if (CurrentPiece == null || IsGameOver || IsPaused)
                return false;

            if (Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X + 1, CurrentPiece.Y))
            {
                CurrentPiece.MoveRight();
                return true;
            }
            return false;
        }

        /// &lt;summary&gt;
        /// テトロミノを下に移動(ソフトドロップ)
        /// &lt;/summary&gt;
        /// &lt;returns&gt;移動できた場合true&lt;/returns&gt;
        public bool MoveDown()
        {
            if (CurrentPiece == null || IsGameOver || IsPaused)
                return false;

            if (Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X, CurrentPiece.Y + 1))
            {
                CurrentPiece.MoveDown();
                LastDropTime = DateTime.Now; // 落下タイマーをリセット
                return true;
            }
            else
            {
                // 下に移動できない場合はピースを固定
                PlaceCurrentPiece();
                return false;
            }
        }

        /// &lt;summary&gt;
        /// テトロミノを回転
        /// &lt;/summary&gt;
        /// &lt;returns&gt;回転できた場合true&lt;/returns&gt;
        public bool RotatePiece()
        {
            if (CurrentPiece == null || IsGameOver || IsPaused)
                return false;

            // 一時的に回転させてみる
            var originalRotation = CurrentPiece.Rotation;
            var originalShape = CurrentPiece.Shape;
            
            CurrentPiece.RotateClockwise();

            // 回転後の位置が有効かチェック
            if (Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X, CurrentPiece.Y))
            {
                return true; // 回転成功
            }
            else
            {
                // 回転できない場合は元に戻す
                CurrentPiece.Rotation = originalRotation;
                CurrentPiece.Shape = originalShape;
                return false;
            }
        }

        /// &lt;summary&gt;
        /// ハードドロップ(一気に底まで落とす)
        /// &lt;/summary&gt;
        /// &lt;returns&gt;落下した距離&lt;/returns&gt;
        public int HardDrop()
        {
            if (CurrentPiece == null || IsGameOver || IsPaused)
                return 0;

            int dropDistance = 0;
            while (Board.CanPlaceTetromino(CurrentPiece, CurrentPiece.X, CurrentPiece.Y + 1))
            {
                CurrentPiece.MoveDown();
                dropDistance++;
            }

            // ピースを固定
            PlaceCurrentPiece();
            
            // ハードドロップのボーナス点
            Score += dropDistance * 2;

            return dropDistance;
        }

        /// &lt;summary&gt;
        /// 現在のテトロミノをゲーム盤に固定
        /// &lt;/summary&gt;
        private void PlaceCurrentPiece()
        {
            if (CurrentPiece == null)
                return;

            // ピースをボードに配置
            Board.PlaceTetromino(CurrentPiece);

            // 完成した行をクリア
            int clearedLines = Board.ClearCompletedLines();
            
            if (clearedLines &gt; 0)
            {
                // スコア計算(一度に多く消すほど高得点)
                int lineScore = clearedLines switch
                {
                    1 =&gt; 100 * Level,   // シングル
                    2 =&gt; 300 * Level,   // ダブル  
                    3 =&gt; 500 * Level,   // トリプル
                    4 =&gt; 800 * Level,   // テトリス!
                    _ =&gt; 0
                };
                
                Score += lineScore;
                Lines += clearedLines;
                
                // レベルアップ判定(10行消去で1レベルアップ)
                Level = Lines / 10 + 1;
            }

            // ゲームオーバー判定
            if (Board.IsGameOver())
            {
                IsGameOver = true;
                return;
            }

            // 次のピースを出現
            SpawnNextPiece();
        }

        /// &lt;summary&gt;
        /// 自動落下処理(時間経過でテトロミノを下に移動)
        /// &lt;/summary&gt;
        public void Update()
        {
            if (IsGameOver || IsPaused)
                return;

            // 落下間隔をチェック
            if (DateTime.Now.Subtract(LastDropTime).TotalMilliseconds &gt;= DropInterval)
            {
                MoveDown();
                LastDropTime = DateTime.Now;
            }
        }

        /// &lt;summary&gt;
        /// ゲームをリセット
        /// &lt;/summary&gt;
        public void Reset()
        {
            Board = new GameBoard();
            Score = 0;
            Lines = 0;
            Level = 1;
            IsGameOver = false;
            IsPaused = false;
            LastDropTime = DateTime.Now;
            
            NextPiece = GenerateRandomTetromino();
            SpawnNextPiece();
        }

        /// &lt;summary&gt;
        /// ゲームの状態を文字列で取得(デバッグ用)
        /// &lt;/summary&gt;
        /// &lt;returns&gt;ゲーム状態の文字列&lt;/returns&gt;
        public override string ToString()
        {
            return $"Score: {Score}, Lines: {Lines}, Level: {Level}, " +
                   $"GameOver: {IsGameOver}, Paused: {IsPaused}";
        }
    }
}</code>

まとめ

今回はここまでとします。
現状では下記の3クラスを作成しました。
1,Tetrimino.cs テトリミノ(落ちてくるブロック)
2,GameBoard.cs ゲーム盤の管理
3,TetrisGame.cs ゲーム全体の制御
次回はController部分を作成します。
では、また次回お会いしましょう!!