見出し画像

Puzzlinkで新エディタを開発してみる日記<クロクローン編④>

前回は「矢印の1マス先のユニットサイズ判定」をやりましたが、Cellオブジェクトの中身やAddressオブジェクト、そしてGraphBase系のオブジェクトと初めて戯れる濃厚な回となりました。
今回は残りの2つのチェック項目を仕上げていこうと思います。

2.checkUnitsCount()

これは「領域内にユニットが2つだけ」を判定するメソッドです。
この手の「各領域で好きな制約条件を調べる」というのは領域を使うパズルならほぼ全てで使うはずの汎用処理なので `src/variety_common/Answer.js` で使えそうなのを探してみます。

行き止まりとなった調査(飛ばして読んでもOK)

checkAllBlock(graph, filterfunc, evalfunc, code)
・概要:
各領域内で「ある条件を満たすマス数」について何かの判定を行う
graph: GraphBase型オブジェクト
 ・今回は領域を管理する board.roommgr を渡してあげればOK
filterfunc: 領域内のうちチェック対象マスだけ抽出する関数 f(c)
 ・引数はCellオブジェクト1個、戻り値はBoolean型
 ・領域内の全マスを対象にする場合は null を指定
evalfunc: 正解判定用の関数 f(w, h, a, n)
 ・w, h は領域の横幅と縦幅(長方形領域の面積を出すのに使ったりする)
 ・a は上記 filterfunc で抽出した後のマス数
 ・n は領域のヒント数字(へやわけや島国の左上の数字など)
 ・戻り値:正解ならtrue、間違いならfalseを返す
code: 間違い検出時のエラーコード

たとえば、「島国」のように「部屋内の数字と黒マスの数が等しいかどうかの判定」は次のように使えばよいらしい。

// src/variety_common/Answer.js: 571-582
checkShadeCellCount: function() {
    this.checkAllBlock(
        this.board.roommgr,     // 領域管理用オブジェクト
        function(cell) {        // フィルタ条件:黒マスかどうか
            return cell.isShade();
        },
        function(w, h, a, n) {  // 正解条件:部屋の数字n = 黒マスの数a
            return n < 0 || n === a;
        },
        "bkShadeNe"             // 不正解時のエラーコード
    );
}

指定のgraphは別に「領域(AreaRoomGraph)」でなくともよく、例えばチェンブロなんかは「黒マスの塊(AreaShadeGraph)」に対して呼び出して各ブロックの大きさ判定などをしたりしています。汎用性が高いメソッドだあ。
でもここまで書いておいて、「ユニット」単位で数えたい今回のケースは対応がぱっと思いつかぬ。行き止まり。ほかに探してみよう。

以下、引数 graphとcodeの使い方は上記と同じなので説明を省略。

checkSameObjectInRoom(graph, getvalue, code)
・概要:領域内のマスがすべてある性質を持つかどうか判定
・getvalue(cell):マスについての何かの整数値を返す。

「ラインダース」とか「ワードロープ」のような「部屋内にすべて同じ要素が入る」という判定はこれでいけそうです。でもクロクローンには無関係。

checkGatheredObjectInGraph(graph, getvalue, code)
・概要:各マスについてgetvalue(c) で何かの整数値を計算、その結果が複数の部屋にばらけて存在しないか(=同じ結果を持つマスが1つの部屋内に収まるか)をチェック。

よくわからないので具体的な使用例を調べたら「お家へ帰ろう」や「ドミニオン」のアルファベット所属判定とかで使われているらしい。なるほどね。でもねえ。

というわけで 結論:自前で実装しなければいけない

2.checkUnitsCount() 仕切り直し

ただいろいろ調べて勉強にはなったので良しとしましょう。行き止まりは人生を豊かにする。うん。

ここでは率直に、
・領域のリスト(board.roommgr.components)を回して
・その中のマスのリスト (r.clist) を回し
・黒マスの場合に所属ユニットのID (c.sblk) を集合型にぶちこみ
・最後に集合の要素数が2か調べる

という流れで行きます。集合型にぶち込むことで、同じIDを何度入れようとしても最初の1回しか入らないようになります。要はかぶりが生じないわけです。
ところで、組み込みのSet型 をそのまま使おうとmakeがエラーを吐いてくるのですが、これはスクリプトの先頭で↓を追記すれば回避できる様子(よくわからんけどESLintの仕様っぽい)。

/* global Set:false */

それで、出来上がった品がこちら。

// Check number of units is 2 for each region
checkUnitsCount: function() {
	var rooms = this.board.roommgr.components;
	for (var i = 0; i < rooms.length; i++) {
		var r = rooms[i];
		var set = new Set();
		r.clist.each(function(cell) {
			if (cell.isShade()) {
				set.add(cell.sblk);
			}
		});
		if (set.size === 2) {
			continue;
		}

		this.failcode.add("bkUnitNe2");
		if (this.checkOnly) {
			break;
		}
		r.clist.seterr(1);
	}
},
たぶん想定通りの挙動。左下は部屋内では2つですが、
部屋外のつながりも見てちゃんと1つ扱いになってくれています。

いい感じですね。ついでに「辺の共有」エラー時に部屋全体を赤くするのではなく当該マスだけ赤くするよう変更しました。
あと「不明なエラー」だと気持ち悪かったので、前回放置した言語データも追加しています (src/res/failcode.json)。

3.checkUnitsShape()

いよいよラスト「ユニットの同型判定」の部分です。というわけで同じく黒マスブロックの同型判定を行っていると思わしき頼れる仲間、`chaindb.js`(チェンブロ)のスクリプトを見に行きます。
するとありました、268行目に isDifferentShapeBlock(block1, block2) メソッドを呼び出しています。この本体は例によって `src/variety_common/Answer.js` にありますので、クロクローンでも使えます。やったね。

形状情報を計算するコア部分は `src/puzzle/PieceList.js` の getBlockShapes() 関数でやられていますが、何やってるのかさっぱり。なんか形を文字列情報に変換しているらしい。
とりあえず、戻り値として辞書が返ってきて、canon属性には回転・反転をある基準で揃えた形状文字列、id属性 にはオリジナルの形状文字列が入ってくる様子。回転・反転含める合同判定は前者を、生の形をそのままの合同判定は後者を使えばよいということですね。

追記23/03/05:内部アルゴリズムのメモ
・形状文字列:外接する長方形領域を用意し、その中のどのマスに実際ブロックがあるかを01の情報で出力。
・長さ8のdata配列には回転・反転の計8パターンの形状文字列が入る。0番がオリジナル、1番が上下反転、2番が左右反転・・・等。
・回転・反転の向きを統一する(=8パターンの中から代表を選ぶ)正規化処理は、data配列をソートして最初の要素を取るというもの。

話が脱線したので時を戻します。
同型判定が一つのメソッドでできるとわかればあとは話は早い。
各領域でユニットのリストを取得するくだりまでは先ほどと共通なので、ここは getUnits(room) というヘルパーメソッドに移行しましょうか。見通しがよくなります。
同型判定時はユニット数=2が成立済みという大前提で、それ以外の場合は無視するようにします。
そんな感じでプログラムを組んでいくと、先ほどのメソッドの変更と合わせて、このようになります。

// Check number of units is two for each room
checkUnitsCount: function() {
	var rooms = this.board.roommgr.components;
	for (var i = 0; i < rooms.length; i++) {
		var units = this.getUnits(rooms[i]);
		if (units.length === 2) {
			continue;
		}
		this.failcode.add("bkUnitNe2");
		if (this.checkOnly) {
			break;
		}
		rooms[i].clist.seterr(1);
	}
},

// Check shapes of the two units for each room
checkUnitsShape: function() {
	var rooms = this.board.roommgr.components;
	for (var i = 0; i < rooms.length; i++) {
		var units = this.getUnits(rooms[i]);
		// check only when the number of units is two
		if (units.length !== 2) {
			continue;
		}
		if (this.isDifferentShapeBlock(units[0], units[1])) {
			this.failcode.add("bkDifferentShape");
			if (this.checkOnly) {
				break;
			}
			units[0].clist.seterr(1);
			units[1].clist.seterr(1);
		}
	}
},

// get all unit component in the room
getUnits: function(room) {
	var set = new Set();
	room.clist.each(function(cell) {
		if (cell.isShade()) {
			set.add(cell.sblk);
		}
	});
	return Array.from(set);
}

それでは、最後にチェック。

いちおうは完成、まだバグありそうだけど。

同型判定もうまく動いてくれていそうです。やったね。

ここまでで
・入力UI (MouseEvent, KeyEvent)
・盤面設定 (Board, Cell, 各種GraphBaseのサブクラス)
・描画 (Graphics)
・URL・ファイル入出力 (Encode, FileIO)
・解答チェック (AnsCheck)

の5要素をクロクローン用にカスタマイズしてきました。よほど新規要素がない限り、基本的にはこの5要素を各パズルのスクリプトで実装してやればよいだけっぽいですね。
練習で作ってみてよかった。だいぶ理解が深まった。

ですがまだこれは完成ではない。
公式のりどみに戻ってみますと、6ステップあるうち、ここまでがステップ2。ひ~。
とはいえ、3はルールページ用に不正解サンプルを用意してあげるだけだし(入力テストや単体テストは既存要素のツギハギなので大丈夫でしょう)。4, 5はトップページにリンクを張ったり背景画像を設定したりの仕上げで、6のChangelogに至ってはここ数年止まっているので多分やんなくていいやつ。

なので、後のステップは次回総仕上げとしてまとめてやってしまおうと思います。
あとはプルリクが承認されればいいのだけれど。

それではまた次回。

この記事が気に入ったらサポートをしてみませんか?