Puzzlinkで新エディタを開発してみる日記<クロクローン編③>
前回はこちら。
本誌182号の目次も公表され、とりあえず第一フェーズは突破したことを確認しました。めでたい。ただマルタリングは何か改造が加わっている様子なので、第二回は本来の目的である「新作」のエディタをいっちゃおうかなと思います(どれかはまだ発売前なのでお伏せ)。
さて、クロクローンの解答チェックを再開しましょうか。
前回は下準備(メソッド名や構成など)を行ったので、今回はいよいよプログラムを書いていきます。
さて、解答チェックでやらないといけないのはあと3つでした。
・checkArrowNumber:矢印マスの先に指定の大きさのユニットがあるか
・checkUnitsCount:各領域にあるユニットがちょうど2つか
・checkUnitsShape:上を前提に、2つのユニットの形が同じか
準備:check系メソッドの一般構成
その前に、一般にcheck系のメソッドがどういう挙動をすべきかをメモ。
以下の this は AnsCheckオブジェクトのことを指すものとします。
・エラーがあったら this.failcode.add(code) でエラーコードの文字列を追加
・this.checkonly フラグ(正解/不正解の確認だけで盤面を赤くしない)が立っていたら、何かエラーをaddした時点で終了。
・そうでなければ間違い個所の盤面要素に seterr(1) を実行、盤面に赤く表示されるようエラーフラグを立てる。
・エラーが何もなかったら this.failcode が空配列になるため、コア部分ではこれが空かどうかで正解判定を行っている。
試しに、島国から流用した checkSideAreaShadeCell を見てみましょう。
境界線をまたいで黒マスが隣接してはいけない、というチェックです。
checkSideAreaShadeCell: function() {
this.checkSideAreaCell(
function(cell1, cell2) {
return cell1.isShade() && cell2.isShade();
},
true,
"cbShade"
);
}
// src/variety_common/Answer.js: 701-725
checkSideAreaCell: function(func, flag, code) {
for (var id = 0; id < this.board.border.length; id++) {
var border = this.board.border[id];
if (!border.isBorder()) {
continue;
}
var cell1 = border.sidecell[0],
cell2 = border.sidecell[1];
if (cell1.isnull || cell2.isnull || !func(cell1, cell2, border)) {
continue;
}
this.failcode.add(code);
if (this.checkOnly) {
break;
}
if (!flag) {
cell1.seterr(1);
cell2.seterr(1);
} else {
cell1.room.clist.seterr(1);
cell2.room.clist.seterr(1);
}
}
},
実質の本体である checkSideAreaCell (f, flag, code) はパズル間で共用可能な `src/variety_common/Answer.js` に定義されている汎用メソッドで、「境界をまたぐ2マス」に関して何らかのチェックを行います。
このファイルにはほかにもこういう「判定用の関数f」と「その時のエラーコードcode」を渡すタイプの汎用メソッドがいくつかありますので一通り目を通しておくとよいのかも。
上のケースでは、2つのマスがともに黒マスかを isShade()メソッド で見て、それに引っかかったら "cbShade" のエラーがfailcodeにaddされるようになります。第二引数のflag=trueはエラー表示で部屋全体を赤くするか該当マスだけ赤くするかの切り替えフラグっぽい。
まとめますと、check系関数の中身は ↓ のような構成になるわけですね。
ただ上のように汎用メソッドを使えば一連の処理は裏でやってくれますので、使えそうなときはなるべく頼ることにします。
checkHoge() {
// 間違ってる場所を探す
if (なんか間違いがあった) {
this.failcode.add("そのエラーコード名");
}
if (this.checkOnly) {
break;
}
赤くする盤面要素.seterr(1)
}
1:checkArrowNumber() メソッド
このメソッドでは「矢印の1マス先に指定の大きさのブロックがあるか」をチェックします。残念ながら頼れる汎用メソッドはなさげなので、yajikazu.jsの矢印チェックを参考に1から組んでいくほかなし。
さて、各Cellオブジェクト(this.board.cell[i] で取得)には
・qnum:問題盤面の数字
・qdir:矢印付き数字の方向(0で無、1,2,3,4は「上下左右」の順)
が属性値としてそれぞれ格納されています。
それで「矢印の1マス先のマス」を取ってくるのは以下のコード。
var bd = this.board;
for (var c = 0; c < bd.cell.length; c++) {
var cell = bd.cell[c];
if (!cell.isValidNum() || cell.qdir === 0) {
continue;
}
var pos = cell.getaddr();
var cell2 = pos.movedir(cell.qdir, 2).getc();
// cell2 にある黒マスの塊のサイズを取得して
// それがcell.qnum と同じかどうか調べる
}
各マスを走査し、それがちゃんとした矢印付き数字マス(空白でも「?」でも数字単体でもない)なら、その1マス先のマスをcell2に格納します。
盤面上の座標計算はAddressオブジェクト(getaddrで取得)を使えば扱えるのですが、これはマス・境界線・格子点を統一の座標系で扱うべく「0.5マス」が基本単位となっています。つまり、1マス先は「2基本単位ぶん先」となり、movedir(矢印方向、2) で矢印の1マス先へいけるわけですね。
さて次はcell2にある黒マスのカタマリのサイズを取得する部分。これはどうすればよいのか。ここからがpuzzlink開発の上での難所であり花形、「つながり」を扱うGraphBaseオブジェクトの出番になります。
GraphBase関連のクラスの整理
まず、基盤にあるクラス系統を整理しておきます。ここらへんは`src/puzzle/GraphBase.js` などで定義されています。
・GraphComponent:いわゆる「1つのグラフ」に相当するクラスで、ノード一覧とその接続関係を保持。つまりは「連結関係の情報を持つ盤面要素のグループ」と考えればよいです。ただし「ひとつながりの要素」とは限らないため注意。
各領域、黒マスや白マスの塊、ループやその断片などはすべてこのクラスのインスタンスになっています。
・GraphBase:上記GraphComponentをまとめて管理するクラス。各コンポーネントのグラフ操作から、コンポーネント間の合体など全部やってくれるクラスで、連結要素関連の処理は基本的にこのオブジェクトを介して行う。
サブクラスに AreaRoomGraph(領域管理クラス)、AreaShadeGraph(黒マスの塊管理クラス)、LineGraph(線のつながり管理クラス)等がある。
さて、これらGraphBaseの(サブ)クラスを使うには、各パズルで明示的に何を使うか設定してやらないといけないようです。
どこでやるかというと、前々回に島国スクリプトから脳死でコピペしてきた、この部分。
AreaRoomGraph: {
enabled: true,
hastop: false
},
なるほど、これのおかげで「領域(太線で区切られた部屋)」が使えるようになっていたわけだね。ヒントが左上に置かれない設定もここでやっていました。
これと同じ要領で「黒マスのつながり」を管理するクラスも使うよう設定し
てあげます。とはいっても基本的に enabled 属性をONにするだけっぽい。
AreaShadeGraph: {
enabled: true
},
これを使えば形の判定とか、部屋の中での個数とかも見通しが立ちそうです。よかった。さっそくどうアクセスできて何ができるのかコンソール相手にいろいろ試行錯誤。
するといい感じに、塊のサイズをとってこれそうでした。
そう分かればあとは一気に行ってしまいましょう。
ちなみに画像の「←3」のように盤面外を向かれてもちゃんと空っぽのCellオブジェクト(isnull属性がtrueかどうかで判別可能)が戻ってきてくれるので、境界の例外処理も楽そうです。たすかる。
// Check unit size pointed by arrow
checkArrowNumber: function() {
var bd = this.board;
for (var c = 0; c < bd.cell.length; c++) {
var cell = bd.cell[c];
if (!cell.isValidNum() || cell.qdir === 0) {
continue;
}
var pos = cell.getaddr();
var cell2 = pos.movedir(cell.qdir, 2).getc();
if (cell2.sblk) {
if (cell2.sblk.clist.length === cell.qnum) {
continue;
}
}
this.failcode.add("anUnitNe");
if (this.checkOnly) {
break;
}
cell.seterr(1);
if (cell2.sblk) {
cell2.sblk.clist.seterr(1);
}
}
},
「不明なエラーです」と出るのは、たぶん言語データ(src/res/failcode.json)を設定していないからだと思います。とにかく、ちゃんと黒マスの数の判定はできていそうです。
さて、気づいたら朝の5時です。寝なければならない。
残りの見通しもだいぶたったところで、ねます。