ペンパ描画ソフトウェアの構想(実装)
これは「ペンシルパズルA Advent Calendar 2019」の5日目の記事です。
4日目はpzdcさんでした。6日目はnyoroppyiさんです。
はじめに
普段はパズルと解いたりたまに作ったりしているpuripuri2100です。こんにちは。 最近では「ドッスンフワリ35*70早解き大会」を主催していました。
他には開成高校のパズル研究部の部長と編集長をしています。 編集長は3年目になります(技術継承しないと……)。
さて、世の中には数えきれないほど多種多様なペンシルパズルがあります。 ニコリに体裁されているパズルであれば「ぱずぷれ」でほとんど画像化と共有ができますし、「penpa-editor(ペンパくん)」を使えばオリジナルパズルだって画像化と共有ができてしまいます。
しかし、この二つのソフトウェアはGUIのWebソフトウェアであり実装もどちらもJava Scriptです。 ペンシルパズル描画ソフトウェアにはもっと多様性があっても良いのではないでしょうか?
そういうわけで今回「リスト構造・代数的データ型・パターンマッチ」等のある程度の機能が備わっている一般的なプログラミング言語であればペンシルパズルを描画できる方法を考案し、実際にSATySFiというPDFを出力するための言語で実装してみました。この方法の一番重要なのは「パズルを全て文字だけでかけるようする」という点です。
今回実装したものはここにおいてあります(メインはこの中のpuzzle.satyh
というファイルです)ので、コードを眺めながらこの記事を読んでいただけるとわかりやすいかもしれません。
ここから先、プログラミング要素がかなり多くなります。よくわからない時は「そういうものなのか~」程度でのんびり読み進めていただけると嬉しいです。
何を描画するか
一口にペンシルパズルと言っても、ヤジリンのようにマスを塗って線を引くものから数独の用に数字を埋めるもの、ハニーアイランドのように6角形のマスのものまで様々です。
この中で、今回は四角いマスを持ちその中で線を引いたり升目を塗ったり丸を描いたり数字や文字を入れたりするパズルを描画できるようにしていきたいと思います。
理由として以下のことが挙げられます。 - マス内に入れるものが数字でも記号でも文字でも労力はあまり変わらない - 線を引けないとかなり多くのメジャーなパズルが描画できなくなるので対応できるようにしたい - 四角形ではないマスを使うパズルはかなり種類が少なく、対応する労力に見合うほどではない
描画する記号類
これに関しては使用する言語がどこまで表現できるかにかかっていますが、SATySFiで実装するものに関しては、ベジェ曲線などを使って描けるものなら何でも描けるようにします。
描画できる文字
こちらも使用する言語とPDFライブラリにかかっていますが、最低でも基本的なアルファベット程度は使えるようにしたいですね。
構想
さて、今回の記事のメインです。
ここではどんな風にすれば大体のペンパを描画できるかを考えていきます。
セルの種類
盤面にはよく見るとセルがたくさんありますが、これらを
- 記号等が入って盤面を構成するセル
- 記号等が入ってパズルを構成するものだが、盤面の外に配置されるセル
- 何も記号が入らず、パズルを構成しない“空白”となるセル
の3つに分類します。
ここでは便宜的にそれぞれInSideCell
・OutSideCell
・NullCell
と名づけます。
OutSideCell
の代表的な使い道としてはキンコンカンでのアルファベットを書く部分でしょうか。
盤面を書く
さて、折角セルの種類を分けてみたので盤面を簡単に作ってみます。 中身の記号類は一旦放置して種類だけでも並べてみましょう。
例えば、このようなぬりみさきは
IIIIII IIIIII IIIIII IIIIII IIIIII IIIIII
のように書けますし、
のようなビルディングは
NNNO OIII NIII NIII
のように書けます。結構簡単ではないでしょうか?
本当はここに書いてあるI
(InSideCell
の略です)やO
(OutSideCell
の略です)に、書いてある数字や白丸についての情報が入るわけですが、すくなくともこれだけで盤面は作れてしまいます。
セルの分け目の線を引く
「セルの分け目の線」って何?と思った人も多いと思います。 簡単に言えば の太線と細線です。
太線は「盤面の内側と外側との境界」ですので、InSideCell
とOutSideCell
が接している部分か、InSideCell
とNullCell
が接している部分と言い換えることができます。
細線は「盤面内のセル同士の境界」なのでInSideCell
とInSideCell
が接している部分となります。
実際に実装する際にはこのように横と縦に一つずつ見ていって隣あう二つのセルの種類で判定をします。この図で青丸のところが外側の境界線となる箇所で、赤丸が内側同士の境界線となる箇所です。
どの座標の境界線がどの種類の境界になるかを判定したらそれぞれデータとして保存しておきます。そして、このデータと、座標を元に線を引く関数を使ってあとで一括で盤面を描画します
さて、この方法を使うと複雑な形の盤面であっても対応することができます(当然ではありますが)のでドーナツ型の盤面を作りたい時などにも使えます。
置く記号の指定
一口に記号と言ってもいくつもの種類があるので、特徴ごとに分けて記号を指定して行きたいと思います。
まずは文字です。
String(セルの中での位置, 文字列)
などのように書けると良いでしょう。
次に黒丸や黒マスなどの記号です。
Object(セルの中での位置, 記号本体の描画)
で行けると思います。ただ、「記号本体の描画」はそれぞれのプログラミング言語とライブラリに強く依存します。
次に線です。
Line(太さなどの線の引き方, 始点の位置, 次の点のセルの相対位置とその中での位置)
です。かなり冗長ですが、できなくはないと思います。
書き方はこれで良いのですが、一つのセルの中に記号が複数入る事はあるので念のためリスト構造にしておきましょう(リスト構造とは、同じ種類のデータを並べる時につかうもので、[1, 2, 3, 4]
などのように書きます)。
さて、これで盤面はいつでも作れるようになりました。 ためしに
を文字で書いてみましょう(出てくる関数や記法は架空のものです)。
InSideCell
はI
に、OutSideCell
はO
に、NullCell
はN
にしています。
[ [I(), I(), I(), I(), I(), I([Object(0.5, 0.5, cycle-num(6))])], [I([Object(0.5, 0.5, cycle-num(2))]), I(), I(), I(), I(), I()], [I(), I(), I([Object(0.5, 0.5, cycle-num(4))]), I(), I(), I()], [I(), I([Object(0.5, 0.5, cycle]), I(), I(), I(), I()], [I(), I(), I(), I(), I([Object(0.5, 0.5, cycle]), I()], [I(), I([Object(0.5, 0.5, cycle-num(3))]), I(), I([Object(0.5, 0.5, cycle]), I(), I()], ]
やや面倒ですが、結構書けますね。 もっと簡単にしたかったら
[ {||||||\cycle{6}|}; {|\cycle{2}||||||}; {|||\cycle{4}||||}; {||\cycle{}|||||}; {|||||\cycle{}||}; {||\cycle{3}||\cycle{}|||}; ]
のような記法から最初のデータを生成するなどすればかなり楽になります。
実装
実装についても書く予定でしたが、間に合いませんでしたすみません……。 追記するかもしれません。
実装ではさらにいくつかの工夫を施していますが、そこまで重要ではないでしょう。
何はともあれオリジナルパズルを文字だけで描けるようになったのは画期的だと思います(個人的に)。