Canvasミニゲームリバーシ

【JavaScript】 最弱リバーシを作る[2]:ゲーム状況の管理

更新日:2022/12/08

『リバーシを作る』の第二回目は、ゲーム状況を管理する機能を作成し、ある程度遊べるものに仕上げてみます。

- 最弱リバーシを作るIndex -
【JavaScript】 最弱リバーシを作る[1]:DOM要素と描画
■【JavaScript】 最弱リバーシを作る[2]:ゲーム状況の管理 ← 今読んでいる記事
【JavaScript】 最弱リバーシを作る[3]:思考ルーチンを組み込む

 

ゲーム状況データの管理

ここでは次の管理をおこないます。

  1. 縦8横8の枠それぞれに、何も置かれていないのか、白か黒かのデータ管理
  2. コマを置ける位置(現在コマが置かれていなくて、相手のコマが取れる位置)の取得
  3. コマを置いたときの裏返し処理(描画呼び出し含む)
  4. 現在の対戦状況の集計

ソースコード


const PLAYER_NONE = Symbol(); // 未使用を表す定数

    // 一定時間待つ関数
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

/**
 * ゲーム状況データの管理
 * @param drawCtrl
 */
const getGameDataManager = ( drawCtrl )=>{

    const pieceNum = option.BORD_PIECE_NUM;

        // ゲーム状況データ作成 横、縦の2次元配列
    const data = Array.from( {length:8},
                             e => new Array(pieceNum).fill(PLAYER_NONE) );

        // 指定位置の状況を取得
    const status = ( col , row ) => data[row][col];
        // 指定位置の状況を更新
    const statusSet = ( col , row ,val ) =>  data[row][col] = val;

        // 指定位置(col , row)から指定方向(stepX , stepY)でコマがとれるかチェック
    const checkPiece = ( col , row ,stepX , stepY , nowPlayer ) =>{
        let x = col + stepX,y = row + stepY;
        let firstPos = null;
        let count = 0; // 取得可能なコマ数

        while( true ){
            if( x < 0 || x >= pieceNum || y < 0 || y >= pieceNum ) return false;
            const stat = status(x,y);
            if( stat === PLAYER_NONE ) return false;

            if( stat === nowPlayer )
                return firstPos === null ? false
                    : [firstPos,   // 始点、
                           [x-=stepX,y-=stepY], // 終点
                           [stepX,stepY],   //  方向
                           count];

            if( firstPos === null ) firstPos = [x,y];
            count ++;

            x += stepX;y += stepY;

        }
    };
        // 指定位置にコマを置けるかどうかチェック
    const pointCheck = ( col , row , player , checkOnly = true) => {

        const piece = status( col , row );
        if( piece !== PLAYER_NONE ) return false;
        const nextPlayer = player === PLAYER_YOU ? PLAYER_COMP : PLAYER_YOU;
        const result = [];

        return ( [[-1,-1],[0 , -1],[1 , -1],[1 , 0],[1 , 1],[0 , 1],[-1 , 1],[-1 , 0]]
                .some( e => {
                    const range = checkPiece( col , row , e[0] , e[1] , player , nextPlayer );
                    if( range !== false ){
                        if( checkOnly ) return true;
                        result.push(range);
                    }
                })
        ) ? true : ( result.length === 0 ? false : result );

    };

    return Object.freeze({
            // ゲーム状況データのリセット
        reset:()=>{data.forEach(e=>e.fill(PLAYER_NONE));drawCtrl.initBord()},
            // コマの配置と描画
        pieceSet:function( col , row , val ){
            statusSet( col , row, val);
            if( drawCtrl !== null ) drawCtrl.drawPiece( col , row , val );
            return this;
        },
            // コマを置ける位置を検索
        getFreePlaces: ( player ) => {
            const result=[];
            for( let row = 0 ; row < pieceNum ; row ++ ){
                for( let col = 0 ; col < pieceNum ; col ++ ){
                    if( pointCheck( col , row , player ) !== false ) result.push( [col,row] );
                }
            }
            return result.length === 0 ? false : result;
        },
            // 現在の状況
        result: ()=>{
            let c1 = 0,c2 = 0,c3 = 0;
            for( let row = 0 ; row < pieceNum ; row ++ ){
                for( let col = 0 ; col < pieceNum ; col ++ ){
                    switch( status( col , row ) ){
                        case PLAYER_NONE:c1++;break;
                        case PLAYER_YOU:c2++;break;
                        case PLAYER_COMP:c3++;break;
                    }
                }
            }
            return [c1,c2,c3];
        },
            // 指定した位置にコマを置き、裏返し可能なものを処理
        clickSets: async function( col , row , player , callBack ){

            const range = pointCheck( col , row , player , false );
            this.pieceSet( col , row , player);
            if( range === false ) return;

            for( let i = 0 ; i < range.length ; i ++ ){
                let [colp , rowp] = range[i][0];
                const [stX , stY] = range[i][2];
                const [endColp , endRowp] = 
                    [range[i][1][0] + stX , range[i][1][1] + stY];

                while( colp !== endColp || rowp !== endRowp ){
                    await wait(500);
                    this.pieceSet( colp , rowp , player);
                    callBack(false,this.result());
                    colp += stX;rowp += stY;
                }
            }
            callBack(true,this.result());
        }
    });
};

ソース解説

最初のwait関数は、asyncキーワードを付加した関数内でawaitキーワードとともに実行すると、一定時間処理を停止することができます。

wait実行例


async func(){
   // ・・・何らかの処理
   await wait( 500 ); // 500ミリ秒待つ
   // ・・・何らかの処理
}

とても簡単な処理に見えますがasync/awaitの組み合わせは少しクセがあり、想定通りの動作をおこなわせるには、知識が必要です。
詳しくは次のページを読んでみてください。
【JavaScript】 async/awaitを解説します

次にgetGameDataManager関数について解説します。

この関数は最初の方で、コマの取得状況を格納する2次元配列を作成し、全ての要素を未使用として初期化します。


        // ゲーム状況データ作成 横、縦の2次元配列
const data = Array.from( {length:8},
                        e => new Array(pieceNum).fill(PLAYER_NONE) );

        // 指定位置の状況を取得
const status = ( col , row ) => data[row][col];
        // 指定位置の状況を更新
const statusSet = ( col , row ,val ) =>  data[row][col] = val;

Array.from()は、イテラブルなオブジェクトや配列のようなオブジェクトから、新たな配列を作成します。
今回は、lengthプロパティに数値を設定しておくと、その数だけ配列を作ってくれるという特性を利用しています。

Array.from()の2番目の引数は、新しく作成される配列の要素を加工する関数です。
mapメソッドと同じイメージですね。
ここで返す配列は、配列のようなものでは困るので、new Array(要素数)で配列を作成し、fillメソッドで要素を初期化しています。

次のstatusとstatusSetは、data配列から要素を取り出し、またはセットしています。
colが列で、rowが行です。

colとrow

data配列でcolは一次元目にするべきか、それとも二次元目にするべきなのかと悩みますが、縦横とも分割数は同じなので、status関数を通してデータを取得するようにしておけば、後は何も気にする必要がありません。

続くcheckPiece関数は、指定された方向に向かって、相手のコマを裏返せるか確認しています。


       // 指定位置(col , row)から指定方向(stepX , stepY)でコマがとれるかチェック
const checkPiece = ( col , row ,stepX , stepY , nowPlayer ) =>{
    let x = col + stepX,y = row + stepY;
    let firstPos = null;
    let count = 0;  // 取得可能なコマ数 

    while( true ){
        if( x < 0 || x >= pieceNum || y < 0 || y >= pieceNum ) return false;
       const stat = status(x,y);
        if( stat === PLAYER_NONE ) return false;

        if( stat === nowPlayer )
                return firstPos === null ? false
                    : [firstPos,   // 始点、
                           [x-=stepX,y-=stepY], // 終点
                           [stepX,stepY],   //  方向
                           count];

        if( firstPos === null ) firstPos = [x,y];
        count ++;

        x += stepX;y += stepY;

    }
};

引数のstepXとstepYは、col、rowからどの方向に向かってチェックするかを指定します。
次の図のように、値は全て0,1,-1で指定します。

クライアント

その方向にチェックしていき、相手のコマをとれないならfalseを返します。
取れる場合は、始点(col、rowの次のコマ)、終点、方向、取得可能なコマ数を順番に配列に格納して返します。

次のpointCheck関数は、チェックしたいコマ位置から全方向に対して、先ほどのcheckPiece関数を呼んでいます。


       // 指定位置にコマを置けるかどうかチェック
const pointCheck = ( col , row , player , checkOnly = true) => {

        const piece = status( col , row );
        if( piece !== PLAYER_NONE ) return false;
        const nextPlayer = player === PLAYER_YOU ? PLAYER_COMP : PLAYER_YOU;
        const result = [];

        return ( [[-1,-1],[0 , -1],[1 , -1],[1 , 0],[1 , 1],[0 , 1],[-1 , 1],[-1 , 0]]
                .some( e => {
                    const range = checkPiece( col , row , e[0] , e[1] , player , nextPlayer );
                    if( range !== false ){
                        if( checkOnly ) return true;
                        result.push(range);
                    }
                })
        ) ? true : ( result.length === 0 ? false : result );

};

この関数は、次の2パターンで使用します。

  1. プレイヤーがクリックした位置にコマを置けるかどうかチェックする
  2. 裏返すべきコマを全て取得する

そのため引数にcheckOnlyを設置し、checkOnlyがtrueの場合は、コマを置くことが可能と判断した時点で結果をリターンしています。

someメソッドはforEachと同じように配列の要素全てに対して実行されます。
ただし、メソッドのコールバック関数がtrueを返した場合、そこで終了となります。

次からは、外部公開するメソッドの定義です。

resetメソッドは、ゲーム状況データを全て未使用で初期化し、盤面を初期化しています。

pieceSetメソッドは、指定位置のデータ更新と、コマの描画をおこなっています。

getFreePlacesメソッドは、盤面全体を検索して、コマを置ける位置を取得しています。

resultメソッドは、ゲーム状況データを状況別に集計して返しています。

clickSetsメソッドは、指定位置のデータを更新して、その位置からとれる相手のコマ全てを裏返しています。


            // 指定した位置にコマを置き、裏返し可能なものを処理
clickSets: async function( col , row , player , callBack ){

    const range = pointCheck( col , row , player , false );
    this.pieceSet( col , row , player);
    if( range === false ) return;

    for( let i = 0 ; i < range.length ; i ++ ){
        let [colp , rowp] = range[i][0];
        const [stX , stY] = range[i][2];
        const [endColp , endRowp] = 
                [range[i][1][0] + stX , range[i][1][1] + stY];

        while( colp !== endColp || rowp !== endRowp ){
                await wait(500);
                this.pieceSet( colp , rowp , player);
                callBack(this.result());
                colp += stX;rowp += stY;
        }
    }
    return this.result();
}

この関数は、aync/awaitで構成されています。
各コマの描画後に少しwaitを挟むことで、簡易的なアニメーション効果を演出しています。

 

テスト

前回と今回のコードを使用して、テストをおこなってみます。

今回のコードは、一応遊べるものになっています。


window.addEventListener( "DOMContentLoaded" , ()=> {

    let player = PLAYER_YOU;
    let passWait = false,running = false,clickSetsWait = false;
    let freeSpace = null;

    const playerChange = () =>{
        player = (player === PLAYER_YOU ? PLAYER_COMP : PLAYER_YOU);
        messageCtrl.turnMessage( player );
        freeSpace = gameData.getFreePlaces( player );
        if( freeSpace === false ){  // 取れるコマがない
                passWait = true;
                messageCtrl.pass( ()=>{
                        passWait = false;
                        playerChange();
                        // freeSpace===nullの場合、どちらもとれず終了
                        // テストバージョンのため対応しない
                });
        }
    }
    const gameScreen =  makeGameBord( "reversi" );
    const messageCtrl =  getMessageControler( gameScreen.message );
    messageCtrl.message("開始ボタンを押してください");

    const controlCtrl =  getControlControler( gameScreen.control,
        ()=>{
            if( passWait ) return;
            if( running && !confirm("最初から始めますか?")) return;
            gameData.reset();
            gameData.pieceSet( 3 , 3 , PLAYER_COMP ).pieceSet( 4 , 4 , PLAYER_COMP )
                .pieceSet( 4 , 3 , PLAYER_YOU ).pieceSet( 3 , 4 , PLAYER_YOU );
            controlCtrl.result(gameData.result());
            running=true;
            player = PLAYER_YOU;
            messageCtrl.turnMessage(player);
            freeSpace = gameData.getFreePlaces(player);
        });

    const gameData = getGameDataManager(
        getMakeDrawingControler( gameScreen.bord.canvas,
            ( pos )=>{
                if( passWait || !running || clickSetsWait) return;

                const [col,row] = pos;
                if( freeSpace.some( e => e[0] === col && e[1] === row ) ){

                    clickSetsWait=true;
                    gameData.clickSets( pos[0] ,pos[1] , player ,
                            result => controlCtrl.result(result)
                        ).then( result => {
                                    controlCtrl.result(result);
                                    clickSetsWait=false;
                                    playerChange();
                                }
                    );

                }
            }
        )
    );
});

「考えています」と出ますが、黒白両方とも自分の番です

更新日:2022/12/08

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。