Javascriptでオセロをつくる 第2回

Javascriptでオセロをつくる 第1回

マス(td)に位置情報を加える

現在作成中のJavascriptオセロでは、盤面をtableで、石を置くマスをtdで表現しています。tdがクリックイベントを捉えて、石を置いたりひっくり返したりという動作を行うのですが、自動的に石をひっくり返すためには隣のマスの状態を知らないといけません。しかしtdには自分の盤面での位置を知る方法がありません。そこで、位置の情報をtdにつけようと思います。
方法は二つあって、IDを使う方法と、独自の属性をつける方法です。IDを使うと$('#ID’)で要素を簡単に取得できます。行(r)列(c)をr1c1のようにすればIDから位置がわかります。でもいちいち文字列を分割するのが面倒です。
独自属性をつくって、それに行と列の値をいれておくことができます。独自属性をつけるときには属性名をdata-で始める必要があります。これをつければ$('td’).attr('data-r’)のように列と行の値が別々に取得できます。この方法は別に相反するものではないので両方使うようにします。

tdに属性値を設定するソース(抜粋)

for(var r = 0 ; r < 8 ; r ++){
    var tr = $("<tr>");
    for(var c = 0 ; c < 8 ; c ++){
        // tdにid, data-r, data-c属性を設定する
        tr.append($('<td><div class="none"></div></td>')
            .attr({'id': 'r' + r + 'c' + c, 'data-r': r, 'data-c': c})
        );
    }
    $('table#board').append(tr);
}

マス目をオブジェクトにする

あるマス(td)がクリックされたとき、そのtdの状態によってプログラムが変化します。マス自体に白石、黒石の有無、や上下左右への情報伝達をさせるために、マスをオブジェクトにします。具体的にはMasuクラスをつくって、それを通じて石の変化をさせるようにします。

Masuクラスの定義 (Masu.js)

class Masu {
    constructor (r, c){
        r = parseInt(r); c = parseInt(c);
        if(r < 0 || r >= 8 || c < 0 || c >= 8) throw 'out of board';
        this.r = r;
        this.c = c;
        this.id = 'r' + r + 'c' + c;
        this.td = $('#' + this.id);
    }

    ishi (){
        var div = $('div', this.td);
        return div.hasClass('black') ? ISHI_BLACK : (div.hasClass('white') ? ISHI_WHITE : ISHI_NONE);
    }

    set (ishi){
        var div = $('div', this.td);  // クラスを変更するdiv要素を取得
        if(ISHI_NONE == this.ishi()){  // 石が置かれていないとき
            div.removeClass('none');

        // 挟んだ石をひっくり返す
        }else{
            div.removeClass(ishi == ISHI_BLACK ? 'white' : 'black');
        }
        div.addClass(ishi == ISHI_BLACK ? 'black' : 'white');
    }
}

othello1.js

const ISHI_BLACK = 1;
const ISHI_WHITE = -1;
const ISHI_NONE = 0;

var ishi = ISHI_BLACK;  // 石の白黒

jQuery(function (){
    for(var r = 0 ; r < 8 ; r ++){
        var tr = $("<tr>");
        for(var c = 0 ; c < 8 ; c ++){
            // tdにid, data-r, data-c属性を設定する
            tr.append($('<td><div class="none"></div></td>')
                .attr({'id': 'r' + r + 'c' + c, 'data-r': r, 'data-c': c})
                .click(function (e){  // tdがクリックされたときの動作
                    var masu = new Masu($(this).attr('data-r'), $(this).attr('data-c'));
                    if(masu.ishi() == ISHI_NONE){
                        masu.set(ishi);
                        ishi *= -1;
                        $('div#status').html((ishi == ISHI_BLACK ? '黒' : '白') + 'の番');
                    }else{
                        masu.set(ishi * -1);
                    }
                })
            );
        }
        $('table#board').append(tr);
    }

    // 初期配置
    new Masu(3, 3).set(ISHI_BLACK);
    new Masu(4, 4).set(ISHI_BLACK);
    new Masu(3, 4).set(ISHI_WHITE);
    new Masu(4, 3).set(ISHI_WHITE);
});

othello1.html (Javascriptを別ファイルにした)

<!DOCTYPE html>
<html>
<head>
<style type='text/css'><!--
    table#board {
        background-color: black;
    }

    table#board td {
        width: 30px;
        height: 30px;
        background-color: green;
        padding: 5px;
    }

    div.black {
        background-color: black;
        width: 30px;
        height: 30px;
        border-radius: 50%;
    }

    div.white {
        background-color: white;
        width: 30px;
        height: 30px;
        border-radius: 50%;
    }
--></style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="masu1.js"></script>
<script src="othello1.js"></script>
</head>
<body>
<table id='board'>
</table>
<div id='status'></div>
</body>
</html>

othello1.htmlの実行結果

下に表示されないときはここをクリック(別ウィンドウが開きます)

自動的に石をひっくり返す

黒で挟まれた白石をひっくり返すというのは人間には簡単にわかるのですが、これをプログラム的に表現するのは意外と難しいです。どうやってひっくり返すか返さないかの判断をするのかを考えてみましょう。
新しく石を置いたところをa0とします。この隣のマスをa1とし、その隣をa2とします。ここでの隣はa0からanは直線状に並んでいるとします。
黒石を-1、白石を1、石が置かれていないところを0とし、anはこのいずれかの値をとるとします。a0からa7が-1, 1, 1, -1, 0, 0, 0, 0という値をとっているとき、a1とa2がひっくり返ります。

a0
a1
a2
a3
a4
a5
a6
a7

a0から順に隣のマスにひっくり返るかどうかを聞いていきます。a1は白なのでひっくり返る可能性がありますが、この並びの中に黒がでてこなければだめです。そこで隣のa2に同じことを聞きます。a2もa1と状況は同じなのでa3に聞きます。a3はa2と色が異なるので、a2にひっくり返っていいと返事をします。a2はそれを聞いてa1にひっくり返っていいと返事をします。

a0
a1
a2
a3
a4
a5
a6
a7
返る? -> 返る? -> 返る? ->
<-返れ <-返れ <-返れ

a3に石が置かれていないときは返れないという返事がきます。a2もそれの返事を戻します。

a0
a1
a2
a3
a4
a5
a6
a7
返る? -> 返る? -> 返る? ->
<-そのまま <-そのまま <-そのまま

anはan-1から"n-1″と"a0の色"を引数として受け取ります。a0と同じ色であればn-1をそのまま返します。異なる色であった場合はan+1に問い合わせた結果を返すようにします。

隣のマス

anの盤面での位置を(rn,cn)と表すことにします(rが行、cが列)。an+1がanの右隣りの場合rn+1=rn, cn+1=cn+1となります。an+1がanの左上の場合rn+1=rn-1, cn+1=cn-1となります。an+1がan+2にたどり着くためには自身のrとcにan-1との差を加えればいいことがわかります。

masu2.jsのソース (reverseメソッドでひっくり返しを行う)

class Masu {
    constructor (r, c){
        r = parseInt(r); c = parseInt(c);
        if(r < 0 || r >= 8 || c < 0 || c >= 8) throw 'out of board';
        this.r = r;
        this.c = c;
        this.id = 'r' + r + 'c' + c;
        this.td = $('#' + this.id);
    }

    ishi (){
        var div = $('div', this.td);
        return div.hasClass('black') ? ISHI_BLACK : (div.hasClass('white') ? ISHI_WHITE : ISHI_NONE);
    }

    set (ishi){
        var div = $('div', this.td);  // クラスを変更するdiv要素を取得
        if(ISHI_NONE == this.ishi()){  // 石が置かれていないとき
            div.removeClass('none');

        // 挟んだ石をひっくり返す
        }else{
            div.removeClass(ishi == ISHI_BLACK ? 'white' : 'black');
        }
        div.addClass(ishi == ISHI_BLACK ? 'black' : 'white');
    }

    reverse (count, a0, dr, dc){
        try{
            var neighbor = new Masu(this.r + dr, this.c + dc);

            if(ISHI_NONE == neighbor.ishi()) return 0;
            if(a0.ishi() != neighbor.ishi()) count = neighbor.reverse(count + 1, a0, dr, dc);

            if(count > 0){
                this.set(a0.ishi());
            }
            return count;

        // 盤面の外のとき0を返す
        }catch(e){
            return 0;
        }
    }
}

othello2.jsのソース

const ISHI_BLACK = 1;
const ISHI_WHITE = -1;
const ISHI_NONE = 0;

var ishi = ISHI_BLACK;  // 石の白黒

jQuery(function (){
    for(var r = 0 ; r < 8 ; r ++){
        var tr = $("<tr>");
        for(var c = 0 ; c < 8 ; c ++){
            // tdにid, data-r, data-c属性を設定する
            tr.append($('<td><div class="none"></div></td>')
                .attr({'id': 'r' + r + 'c' + c, 'data-r': r, 'data-c': c})
                .click(function (e){  // tdがクリックされたときの動作
                    var masu = new Masu($(this).attr('data-r'), $(this).attr('data-c'));
                    if(masu.ishi() == ISHI_NONE){
                        masu.set(ishi);
                        for(var dr of [-1, 0, 1]){
                            for(var dc of [-1, 0, 1]){
                                if(dr == 0 && dc == 0) continue;
                                masu.reverse(0, masu, dc, dr);
                            }
                        }
                        ishi *= -1;
                        $('div#status').html((ishi == ISHI_BLACK ? '黒' : '白') + 'の番');
                    }else{
                        masu.set(ishi * -1);
                    }
                })
            );
        }
        $('table#board').append(tr);
    }

    // 初期配置
    new Masu(3, 3).set(ISHI_BLACK);
    new Masu(4, 4).set(ISHI_BLACK);
    new Masu(3, 4).set(ISHI_WHITE);
    new Masu(4, 3).set(ISHI_WHITE);
});

othello2.htmlの実行結果

下に表示されないときはここをクリック(別ウィンドウが開きます)

無効な置き方を禁止する

オセロで石を置けるのは、(1)石がないところ、(2)1つ以上の石をひっくり返せるところ、です。この条件を満たすところだけに置けるようにします。reverse関数に新しい引数(exec)をつけて、これがfalseのときはひっくり返る石の数を返すだけにします。trueのときは実際に石をひっくり返します。
8方向を調べるのが2回になったので、これもMasuクラスにいれました(roundReverse)。

masu3.jsのソース

class Masu {
    constructor (r, c){
        r = parseInt(r); c = parseInt(c);
        if(r < 0 || r >= 8 || c < 0 || c >= 8) throw 'out of board';
        this.r = r;
        this.c = c;
        this.id = 'r' + r + 'c' + c;
        this.td = $('#' + this.id);
    }

    ishi (){
        var div = $('div', this.td);
        return div.hasClass('black') ? ISHI_BLACK : (div.hasClass('white') ? ISHI_WHITE : ISHI_NONE);
    }

    set (ishi){
        var div = $('div', this.td);  // クラスを変更するdiv要素を取得
        if(ISHI_NONE == this.ishi()){  // 石が置かれていないとき
            div.removeClass('none');

        // 挟んだ石をひっくり返す
        }else{
            div.removeClass(ishi == ISHI_BLACK ? 'white' : 'black');
        }
        div.addClass(ishi == ISHI_BLACK ? 'black' : 'white');
        return this;
    }

    // 置いた石を除く
    remove (){
        $('div', this.td).removeClass('white').removeClass('black').addClass('none');
        return this;
    }

    roundReverse(exec){
        var count = 0;
        for(var dr of [-1, 0, 1]){
            for(var dc of [-1, 0, 1]){
                if(dr == 0 && dc == 0) continue;
                count += this.reverse(0, this, dc, dr, exec);
            }
        }
        return count;
    }

    reverse (count, a0, dr, dc, exec){
        try{
            var neighbor = new Masu(this.r + dr, this.c + dc);

            if(ISHI_NONE == neighbor.ishi()) return 0;
            if(a0.ishi() != neighbor.ishi()) count = neighbor.reverse(count + 1, a0, dr, dc, exec);

            if(exec && count > 0){
                this.set(a0.ishi());
            }
            return count;

        // 盤面の外のとき0を返す
        }catch(e){
            return 0;
        }
    }
}

othello3.jsのソース

const ISHI_BLACK = 1;
const ISHI_WHITE = -1;
const ISHI_NONE = 0;

var ishi = ISHI_BLACK;  // 石の白黒

jQuery(function (){
    for(var r = 0 ; r < 8 ; r ++){
        var tr = $("<tr>");
        for(var c = 0 ; c < 8 ; c ++){
            // tdにid, data-r, data-c属性を設定する
            tr.append($('<td><div class="none"></div></td>')
                .attr({'id': 'r' + r + 'c' + c, 'data-r': r, 'data-c': c})
                .click(function (e){  // tdがクリックされたときの動作
                    var masu = new Masu($(this).attr('data-r'), $(this).attr('data-c'));
                    if(masu.ishi() == ISHI_NONE){
                        var count = masu.set(ishi).roundReverse(false);
                        if(count > 0){
                            masu.roundReverse(true);
                            ishi *= -1;
                            $('div#status').html((ishi == ISHI_BLACK ? '黒' : '白') + 'の番');
                        }else{
                            masu.remove();
                        }
                    }
                })
            );
        }
        $('table#board').append(tr);
    }

    // 初期配置
    new Masu(3, 3).set(ISHI_BLACK);
    new Masu(4, 4).set(ISHI_BLACK);
    new Masu(3, 4).set(ISHI_WHITE);
    new Masu(4, 3).set(ISHI_WHITE);
});

othello3.htmlの実行結果

下に表示されないときはここをクリック(別ウィンドウが開きます)

アンドゥ(やり直し)機能をつける

次回に続く。