C#で2048ゲームのクローンを作る[その4]

今日は、前回の予定通り盤面の管理を別クラスに分けてみました。
リファクタリングを行っただけなので、前回のプログラムとできることは全く同じです。


まずは、画面を管理しているForm1.csファイルです。
盤面管理をBoardManagerに逃がしたので、非常にシンプルになりました。

using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
 
namespace Mock2048 {
    public partial class Form1 : Form {
        BoardManager boardMgr;
 
        public Form1() {
            InitializeComponent();
 
            // ボードマネージャの生成
            boardMgr = new BoardManager();
        }
 
        //*********************************************************************
        /// <summary> 画面表示時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_Load( object sender, EventArgs e ) {
 
            // Formコントロール自身がキー入力を取得可能とする
            this.KeyPreview = true;
 
            //盤面の初期化
            boardMgr.initBoard();
 
            // 盤面を表示する
            boardMgr.displayBoard(txtLog); 
        }
 
 
        //*********************************************************************
        /// <summary> キー入力時のハンドラ
        /// </summary>
        //*********************************************************************
        private void Form1_KeyDown( object sender, KeyEventArgs e ) {
            DIR direction;
 
            // ゲームオーバーのときは何もしない
            if (boardMgr.IsGameOver) {
                return;
            }
 
            // スライド方向を判定する
            switch (e.KeyCode) {
                case Keys.Up:    direction = DIR.UP;    break;
                case Keys.Right: direction = DIR.RIGHT; break;
                case Keys.Down:  direction = DIR.DOWN;  break;
                case Keys.Left:  direction = DIR.LEFT;  break;
                default:
                    // 矢印キー以外は無視
                    return; 
            }
 
            // 指定された方向に移動させる
            bool isMove = boardMgr.slideCell( direction );
            if ( !isMove ) {
                // スライドさせてみたが1セルも動かなかった -> 何もしなかったとみなす
                return;
            }
 
            // 盤面を表示する
            boardMgr.displayBoard(txtLog);
        }
    }
}





次にBoardManager.csです。
こちらは、メソッドを移した上でpublic/privateのスコープを見直しています。

using System;
using System.Collections.Generic;
using System.Diagnostics;
 
public enum DIR {
    UP,
    RIGHT,
    DOWN,
    LEFT,
};
 
public class BoardManager {
    const int BOARD_SIZE = 4;
 
    // ゲームの状態(ゲームオーバーしたか否か)
    private bool isGameOver = false;
    public  bool IsGameOver {
        get{ return this.isGameOver; }
    }
 
    // 乱数
    System.Random rand = new Random();
 
    // 盤面の情報
    private int[,] board = new int[BOARD_SIZE,BOARD_SIZE];      
 
    //*********************************************************************
    /// <summary> 盤面の初期化を行う
    /// </summary>
    //*********************************************************************
    public void initBoard() {
        isGameOver = false;
 
        // ランダムに2マス埋める
        for (int loop = 0; loop < 2; loop++) {
            _addCell();
        }
    }
 
    //*********************************************************************
    /// <summary> 盤面を画面に表示させる
    /// </summary>
    //*********************************************************************
    public void displayBoard( System.Windows.Forms.TextBox displayArea ) {
 
        // ゲームオーバーのときは描画しない
        if (isGameOver) {
            displayArea.Text = "GAME OVER";
            return;
        }
 
        string boardData = "";
 
        // すべての行のデータを出すまで繰り返し
        for (int y = 0; y < 4; y++) {
            // 1行分のデータを出力
            for (int x = 0; x < 4; x++) {
                boardData += "[" + String.Format("{0, 2}", board[x,y]) + "] ";
            }
            boardData += Environment.NewLine;
            boardData += Environment.NewLine;
        }
 
        displayArea.Text = boardData;
 
        log( "** Boardを描画します**" );
        log( boardData );
 
        displayArea.Select(0,0);
        displayArea.ReadOnly = true;
    }
 
    //*********************************************************************
    /// <summary> 指定された方向に移動させる
    /// </summary>
    /// <param name="keyCode"></param>
    //*********************************************************************
    public bool slideCell( DIR dir ) {
        bool isMove = false;
        bool retVal;
 
        // 入力されたキーを判定する
        switch ( dir ) {
            case DIR.UP:
                // 2~4列目に対し上方向のスライドを試みる
                for (int y = 1; y < BOARD_SIZE; y++) {
                    for (int x = 0; x < BOARD_SIZE; x++) {  
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.DOWN:
                // 1~3列目に対し下方向のスライドを試みる
                for (int y = BOARD_SIZE-2; y >= 0; y--) {
                    for (int x = 0; x < BOARD_SIZE; x++) {  
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.LEFT:
                // 2~4行目に対し左方向のスライドを試みる
                for (int y = 0; y < BOARD_SIZE; y++) {
                    for (int x = 1; x < 4; x++) {
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            case DIR.RIGHT:
                // 1~3行目に対し右方向のスライドを試みる
                for (int y = 0; y < BOARD_SIZE; y++) {
                    for (int x = BOARD_SIZE-2; x >= 0; x--) {
                        retVal = _moveCell( x, y, dir );
                        isMove = (retVal==true) ? true : isMove;
                    }
                }
                break;
            default:
                //txtLog.Text = "";
                break;                        
        }
        log( "セルをスライドさせました 方向=" + dir.ToString() + " 移動あり?=" + isMove );
 
 
        // セルを追加する
        bool isSuccess = _addCell();
        if (!isSuccess) {
            // セルが追加できない(=空セルがない)時は、ゲームオーバーとみなす
            isGameOver = true;
        }
 
        // 追加した結果、まだスライド可能かチェック
        if (!_canSlide()) {
            // どの方向にもスライドできない場合は、詰みなのでゲームオーバー
            isGameOver = true;
        }
 
        return isMove;
    }
 
 
    //*********************************************************************
    /// <summary> 空いているところに、1つセルを追加する
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private bool _addCell() {
 
        // あいているセルを1つ取得する
        Tuple<int,int> freePos = _getEmptyCell_AtRandom();
        if (freePos == null) {
            return false; // 空セル無し
        }
 
        // 取得した空セルにセットする
        int val = rand.Next(1,3) * 2;
        board[freePos.Item1,freePos.Item2] = val;
 
        log( "  -> セットする値=" + val );
 
        return true;
    }
 
 
    //*********************************************************************
    /// <summary> 空セルをランダムで1つ取得する
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private Tuple<int,int> _getEmptyCell_AtRandom() {
        List<Tuple<int,int>> emptyCellList = new List<Tuple<int,int>>();
 
        // 空セルを全部Listに集める
        for (int y = 0; y < BOARD_SIZE; y++) {
            for (int x = 0; x < BOARD_SIZE; x++) {
                if ( board[x,y] == 0 ) {
                    emptyCellList.Add( new Tuple<int,int>(x,y) );
                }
            }
        }           
        if (emptyCellList.Count <= 0) {
            // 空セルが1つも無い! ->nullを返して終了。
            return null; 
        }
 
        // 空セルの中から1つをランダムで抽選する
        int offset = rand.Next(0, emptyCellList.Count-1);
        Tuple<int,int> emptyCell = emptyCellList[offset];
 
        log( "空セルを取得しました [" + emptyCell.Item1 + ", " + emptyCell.Item2 + "] 残りの空数=" + (emptyCellList.Count-1) );
        return emptyCell;
    }
 
 
    //*********************************************************************
    /// <summary> セルのスライドが可能がチェックする
    /// </summary>
    /// <returns></returns>
    //*********************************************************************
    private bool _canSlide() {
        // 上下に連続して同じ数字があるかチェック
        for (int x = 0; x < BOARD_SIZE; x++) {
            for (int y = 0; y < BOARD_SIZE; y++) {
                if (board[x, y] == 0) {
                    // 空セルがある -> スライド可能
                    return true;
                }
 
                if (x != BOARD_SIZE-1 && board[x, y] == board[x+1, y]) {
                    // 右に同じ数字が続く場所がある -> スライド可能
                    return true;
                }
                if (y != BOARD_SIZE-1 && board[x, y] == board[x, y+1]) {
                    // 下に同じ数字が続く場所がある -> スライド可能
                    return true;
                }
 
            }
        }
 
        // ここまできた -> どの方向にもスライドできない
        return false;
    }
 
 
    //*********************************************************************
    /// <summary> 指定された方向にセルをスライドさせる
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="dir"></param>
    //*********************************************************************
    private bool _moveCell( int x, int y, DIR dir ) {
        int endX;
        int endY;
        bool isDuplicate;
 
        // 移動先のセル位置を求める
        _getDestPos( x, y, dir, out endX, out endY, out isDuplicate );
 
        // セルを移動
        int targetVal = board[x,y];
        board[x,y] = 0;
        board[endX,endY] = targetVal * (isDuplicate ? 2 : 1 );
 
        // セルが動いたか否かを返す
        bool isMove;
        if (x == endX && y == endY) {
            isMove = false;
        } else {
            isMove = true;
        }
 
        return isMove;
    }
 
 
    //*********************************************************************
    /// <summary> 指定された方向にセルをスライドさせた時の移動先を求める
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="dir"></param>
    /// <param name="endX"></param>
    /// <param name="endY"></param>
    /// <param name="isDuplicate"></param>
    //*********************************************************************
    private void _getDestPos( int x, int y, DIR dir, out int endX, out int endY, out bool isDuplicate ) {
 
        // initialize
        endX = x;
        endY = y;
        isDuplicate = false;
 
        // 指定されたセルの値を取得
        int targetCellVal = board[x,y];
        if (targetCellVal == 0) {
            return;
        }
 
        // 移動方向を元に探索方向のオフセットを決める
        int dx = 1;
        int dy = 1;
        switch ( dir ) {
            case DIR.UP:    dx =  0; dy = -1; break;
            case DIR.RIGHT: dx =  1; dy =  0; break;
            case DIR.DOWN:  dx =  0; dy =  1; break;
            case DIR.LEFT:  dx = -1; dy =  0; break;
        }
 
 
        // 何かにぶつかるまで探索する
        while (true) {
            if (endX + dx < 0 || endY + dy < 0 || endX + dx >= BOARD_SIZE || endY + dy >= BOARD_SIZE) {
                // 壁にぶつかった -> 1つ手前の場所でストップ
                isDuplicate = false;
                break;
            } else if (board[endX + dx, endY + dy] != 0) {
                // 他のセルにぶつかった
                if (targetCellVal == board[endX + dx, endY + dy]) {
                    // 衝突先が同じ値だった -> 重ねる
                    endX += dx;
                    endY += dy;
                    isDuplicate = true;
                    break;
                } else {
                    // 衝突先が異なる値だった -> 1つ手前の場所でストップ
                    isDuplicate = false;
                    break;
                }
            } else {
                // 何にもぶつからなかった -> さらに次の位置をチェック
                endX += dx;
                endY += dy;
                continue;
            }               
        }
        //log( "セルの移動を行います [" + x + ", " + y + "] -> [" + endX + ", " + endY + "] dup=" + isDuplicate );
    }
 
    //*********************************************************************
    /// <summary> デバッグログを出力する
    /// </summary>
    /// <param name="message"></param>
    //*********************************************************************
    private void log(string message) {
        Debug.WriteLine( message );
    }
}




次回は、ボードに表示されるセル(数字が表示されている駒)のクラス管理と、描画処理を見直す予定です。

関連記事

コメントを残す

メールアドレスが公開されることはありません。