落ちものパズルをつくる


img
P6はとっても遅いです。
CPU 自体は 4MHz なのに、中の人がサボっているんじゃないかというくらい遅い。
少なくとも筆者の力では往年のアクション全開なゲームとか作れそうにありません。

幸いなことに P6mkII/66 以上では色数が豊富で 15色使えます。
これを有効に活用しつつあまり動きがなく処理が軽そうなもの…
ということで固定画面の落ちものパズルをやってみようということに。



■ そしてそれから

…ただまあ、ルールを再現するだけなら割とサクっと出来てしまうんですね。
テトリスなども1画面とか7行(!)とか、バイト数を極限まで削ったプログラミングのネタにされてきました。
なので逆に、昔なら省略されたであろう部分を敢えて作り込んでみよう、というアプローチを採ることにします。
それによって、P6で実現可能だけど見たことの無かったような表現という新鮮味が出てくるのではないかと。
限界に挑戦とまでは言いませんが CPU にはそれなりに頑張ってもらわねばなりません。


中途半端なソースコードのみ。コンパイルしても↓の画像のようにはなりません。
otip6_20131030.zip







■ 全てのグラフィックツールをP6対応にする

エ?知ってる?そんなの常識?

P6mk2 のScreen Mode 3 は 15色使えるものの、解像度が 160x200 でドットが横長です。
Windowsマシンなどで、何か絵を描いて P6 用にコンバートしようとすると、ヨコ解像度を半分にした上で
変換しなければなりません。まあ、そのへんのツールはササっと作れるのですが(既存のものもある)
そもそも描くときにヨコ幅2倍 320x200 で描くのは手間ですよね。かといって最初から 160x200だととても描きにくい。

ツールでのエディット時 実際にP6上で表示される時

そんなわけで、おもむろにビデオカードのオプション設定を呼び出します。
解像度の追加→カスタム解像度の作成です。
モニターの解像度が 1920x1080なら、ヨコを半分にして 960x1080 というカスタム解像度を作成します。


もうおわかりですね。
作業するマシン自体をドット比 2:1 にしてしまうのです。
もちろんグラフィックツールの解像度も 2:1 。これで P6 と同じ環境で作業できます。
(マウスの横移動が2倍になるので操作は若干しにくくなりますが…)


左が作業風景(アスペクト比2:1)、右が元の解像度(アスペクト比1:1)の状態。
まあ最初からアスペクト比を弄れるグラフィクツールがあればいいんですけどね。

注:カスタム解像度を作れないビデオチップやモニターによっては不可能かもしれません。

■ 画像を圧縮してみる

突然ですが Exomizer というツールがあるんですよ。
MSX 関連を巡っていて見つけた圧縮ツールの一種なんですが、解凍プログラムが Z80版で用意されているのです。
解凍速度も結構速いし、なにより圧縮率が優れています。解凍用のコードもコンパクト。


B型同盟さんの「パピ通」より)
この画像のP6mk2 Mode3 VRAM生データを各ツールで圧縮した場合の圧縮率。
素のデータzip 圧縮Lv9lzh lh7形式 Exomizer
16,000 bytes11,44911,442
11,175

P6mk2(mode3) の画像フォーマットは 1色=4bitで、その 4bit の下位 2bit と上位 2bit を VRAM1 と VRAM2 に分けて記録することになっています。
まず下位の VRAM1(8000byte) があり、192byteの空きの後 VRAM2(8000byte)と続きます。
全体では 16000byte なのですが、考えてみると VRAM1 と VRAM2 は相関があるんですよね。
1 つの色情報をわざわざ 2 つにわけてあるのですから。

というわけで、これを元に戻して 4bit をずらっと配置していくフォーマットを圧縮するとどうなるかというと…。
素のデータziplzhExomizer
16,000 bytes8,1218,1457,905

半分以下というのは凄いですね。
まあ Exomizer が一番良い、ということでもなくて辞書サイズが云々…と適材適所もあるのだと思いますが。

色情報を元に戻してから圧縮する方法は、単純なランレングス圧縮でも圧縮効果が上がると思います。
ただし、展開するのが若干面倒なんですよね。ビット操作だらけでメモリも食います。

圧縮するには >exomizer raw [source] -o [dest] という感じで指定します。
Exomizer はもちろん画像以外にも使えます。
ディスクからバッファに溜め込んだデータを逐次解凍みたいなことが出来ると嬉しいのですが、そこまでは出来ないのかな??

今回は読み込み速度と展開速度を秤にかけて、圧縮は見送り。残念。


■ (変な)音源ドライバをつくる

何かつくろうと思うと音源ドライバは必要ですよね。一家に一本。使い勝手の良い奴が。
求められる条件としては

1)オブジェクトを含め、全体的にサイズがコンパクト
2)軽い。本編の邪魔をしない程度に軽い。
3)そこそこ高機能。ピッチベンド・ソフトエンベロープにセルフディレイまであればいいかな。
4)曲オブジェクト作成にかかる手間が面倒でない。
5)随時、効果音が鳴らせる。
6)ドライバ呼び出しに掛かる手間が楽。組み込みの簡便さにも通じるところがあるかも。
99)そのたいろいろ。

「組み込み向け」という条件を付けると、どうしても脇役にならざるをえないので
どこかで妥協点を探すことになります。高機能すぎても重くなったのでは仕方ないし、
軽さを追求する余りカスタマイズしまくりで取り回しが厳しいのもアレかな、と。

加えて、売りになるものが無い平凡なものを作ったのでは、他所から持ってきたドライバ以上には
なりえないので、結局「車輪の再発明」状態になってしまいます。無駄に MML 文法が増えても仕方ないですね。
個人的には海外の AtariST のサウンドに見るような、国産 MML 環境から離れたような音が
聞きたかったりするのですが、筆者の技術では到底無理。

で、P6 で出来る新しい音源ドライバってなんだろう?と思うに、
・音声合成と同期
これじゃないかと。
まあ筆者の技術では無理なんですけどね(あれれ)。

とはいえ、まだやるべき事は残っているのです。
PSG最後のフロンティアは「非矩形波」にあり!作りもしない内から偉そうですね。

以下の記事はMSX PLUG で有名な Digital Sound Antiques さんの(以前の)記事を参考にしています。平たく言うと受け売りです。
Wikipediaにも少し書いてありますね。

PSGの発声は下の図のようにくし形(矩形波)になっており、デューティー比を弄れない機種(AY-3-8910/YM2149など)では平板なピーという音しか出せません。

これにハードウェアエンベロープを加えると以下のようになります(あくまで模式図なのでいいかげんです)。


さて、PSGのレジスタ 7 はミキシングコントロールになっており、ノイズ・トーンの設定が出来ます。
ここで、ノイズ・トーンとも「オフ」にして発音を停止する(両方のビットを立てる)と信号レベルではどうなるかというと

ハイレベルの状態でフラットになってしまうのです。
(音は振幅(周波数)があって初めて音として認識されるのでこの状態では何も聞こえません。)
ここで上記のハードエンベロープを加えた状態と比較すると・・・上記ではくし形でしたが、どうなるでしょうか。

ハードエンベロープの形状がそのまま出てしまうことになります。立派な三角波です。
エンベロープ形状に繰り返しのある波形を選べば継続して振幅を表現できることになり、その振幅が音程になります。

具体的な形状としては、ノコギリ波(8または12)・三角波(10または14)ということになります。
振幅は、音量16段階を1周期として 入力クロック ÷ 256 ÷ 16bit_Envelope_Period で行うことになっています。
三角波は上がって下がる(下がって上がる)で1周期なので、32段階として 2倍で考えます。

なお、PSGの音量はエンベロープも含め、線形になっておらず偏りのあるカーブを描いています。

よって、ノコギリ波・三角波といってもイメージ通りの音が出るわけではありません。
さらに、上記のエンベロープ周期を音程を表現する目的として使うには精度が粗すぎて、
「おいしく」使える部分はかなり狭い範囲ということになります。
原理上、音量も変えられません。

以上、少し変わった音が出る、というお話でした。

使えるのか使えないのかいまひとつ分からない機能を加えて、とりあえず音源ドライバを作ってみたのですが
最初に挙げたような軽くて使いやすいドライバという目標は遠いです。
さすがに片手間に PSG/SCC/FM の3種類を作るのは無理があったかもしれません。
まあ、非矩形波を鳴らしたくてドライバ作ったようなものなので、それだけで満足なんですけどね。


■ フロッピーディスクを扱う

世間では 3.5インチフロッピーの国内生産が終了というニュースが流れていましたが、
ハードと共に取り扱いに関する情報も失われていくのは残念ですね。

いまさら何か書くのもためらわれる所ではありますが、エミュレータでの扱いのこともありますし、
メモ書き程度に残しておくことにします。


ディスクの構造は上の図のようになっています。
あくまで P6 で使用する「標準的な 1D フォーマット」の場合です。
変態フォーマットは扱う機会も考える必要もないかと思います。

外周から 1 周ずつトラックという単位になっていて、最内周までで全部で 40 トラック。
1周(1トラック)を 16 分割した単位がセクタで、1セクタ内には 256 バイトが格納できます。
注意点として、セクタ番号は 0-15 でなく 1-16 ということです。

全部で 256 * 16 * 40 = 163,840 バイト書き込めるということになります。
フロッピードライブ側に読みだしや書き込みを指示する際にも、「どのドライブの」 「どのトラックの」「どのセクタ」という指定を行うことになります。
セクタより細かい指定はできないので、一度に読み書きできる単位は 256バイトです。

もうちょっとメジャーな 2D,2DD,2HD などのディスクでは、表面だけでなく裏面(サーフェス1)も使います。
この時、トラック番号は、表・裏・表・裏と交互に使っていきます。トラック数は 2 倍の、全部で 80 トラックです。

エミュレータでよく使われる D88 イメージは 2D は正式にサポートしているのですが、
1D はサポートされていないようなので、2D のフォーマットに間借りする形で表します。
すなわち、「表サーフェスだけ書き込んである 2D ディスク」といった感じです。

D88 イメージをバイナリエディタで開いたところです。
トラックテーブルの所で、4バイトずつのアドレステーブルになっていますが、
トラック 0 の次が 4バイト空きになっています。トラック1の次も同じ。一つずつ飛んでいるのが分かると思います。
これは裏サーフェスを使っていないということを意味します。
よって 1D のトラック番号は 2D フォーマットにおいては 2倍で考える必要があります。
最終のトラック 39 は 2D ディスクにおけるトラック 79 の場所にアドレスが書き込んであります。


※ 2012.03追記
同梱していた D88TOOL にバグがあったので修正しました。
ディスクイメージ作成時、セクタ ID の CHRN の CH に書き込む値が間違っていたというものです。
長らく気づかず大変ご迷惑をおかけしました。
ついでに 1DD サポートや BASIC との連携を深めるために機能を追加しました。
・1D/1DD イメージ作成
・セクタ単位指定でバイナリ書き込み(従来通り。1DD を新たにサポート)
・BASIC で扱うプログラムファイルをディスクイメージにアスキーセーブ/BSAVE 形式で書き込み
・BASIC ファイルの KILL / FILES
なお、BASIC をについてはディスク全体を 35トラックとして扱います。詳細は readme.txt をご覧下さい。


・IPL

トラック 0 セクタ 1 は IPL (Initial Program Loader)が入ります。
ディスクドライブが接続され、ディスクを入れた状態でマシンを起動すると、
最初にこのセクタを読みにいきます。

このセクタ 256 バイトの先頭に "SYS"の 3 文字が書き込まれていると、
$F900-$F9FF のメインメモリに IPL をロードし、"SYS"の次(=$F903)からプログラムを実行します。

256(-3)バイトだけではたいしたことは出来ないので、そこからさらに他のプログラムをロードできるように
ディスク読み込みサブルーチンを IPL に配置して、より大きなプログラムをメモリに展開していきます。

ディスクコントローラーへは I/O ポートからコマンドを発行してやりとりを行います。
コマンドはリード・ライト他フォーマット等いろいろありますがここでは割愛。
p6v のソースコード disk.cpp が大変参考になります。

入出力は一旦作ってしまえば、頻繁に作り替えるということも無いと思うので、
今回作ったようなもので十分実用になるのではないかと思います。

外付けの 1Dドライブはインテリジェントタイプなので 88 のようにプログラムを送り込んで使いたいところ
なのですが、どうやらそれはできないようです。

※ 2012.03追記
MODE 5 以上であれば、上記の方法でディスクのオートスタートができるのですが、MODE 4 以下(PC60拡張ベーシック)では
オートスタートに対応していないので、一旦 BASIC を起動してから LOAD または BLOAD する必要があります。

IPL から独自のディスクローダーを起動できる MODE 5 と違って BASIC を経由するとなると、ディスク上のファイルの管理方法が全く違うので
少なくとも BASIC から起動する最初のプログラムだけは BASIC 方式でのイメージ書き込み手順が必要になるわけです。

BASIC のファイル管理はクラスタという単位で行っています。
1D ディスクでは 8 セクタ = 1 クラスタという換算で、ディスク 1 枚では 35 トラック * 16セクタ = 560 セクタ を 70 クラスタとして管理します。
何故新たにクラスタなどという単位にするかというと、560 もあったのでは細かすぎるからです。
どうせ 1 セクタ 256 バイト以内で済むようなプログラムはほとんど無いので、ある程度おおざっぱでも良いということでもあります。

このクラスタという単位でのディスク管理ですが、『そのクラスタはどのファイルが使用中か』『使用中ならば次はどのクラスタに繋がるか』
という情報を管理します。そのために必要な記録領域がディレクトリや FAT という特別な領域です。
この管理領域は 1D の場合クラスタ番号 36,37 (トラック18全部)、1DD の場合クラスタ番号 74,75(トラック 37全部)が充てられます。
ちょうどディスクの真ん中辺りを 1周まるごと使うわけです。邪魔くさいです。

管理領域として使われるトラック中、セクタ 1-12 をディレクトリ、セクタ 13 を ID、セクタ 14-16 を FAT として使います。
このうち重要なのはディレクトリと FAT です。(P6 では ID は使っているかどうか不明、0x00 で埋められています。)
以下はディレクトリの一部(1D ディスクのトラック18セクタ1) を抜粋したものです。



16byte で 1 ファイルの情報が表されています。
最初の 9byte がファイル名、次の 1byte がファイルの属性、その次がファイルの格納開始クラスタ番号です。
先頭が 0x00 になっているのは消去されたファイルで、これは FILES しても表示されません。
属性は 0x80 が BASIC(中間言語形式=普通の SAVE)0x00 がアスキーセーブ、0x01 が機械語 BSAVE を表しています。
BASIC がディスクからロードするときは、該当ファイルの開始クラスタを調べ、次に FAT を調べに行きます。
以下は 1D ディスクの FAT、トラック 18 セクタ 14 を抜粋したものです。



1D なので全部で 70 クラスタの使用状況が記されています。
0xFF は未使用領域です。
0xFE となっている部分が 2 つありますが、これはクラスタ 36,37 つまり管理領域自身です。
それ以外の数値は『次に繋がるクラスタ番号』あるいは『ファイルの最後尾クラスタ』を指し示しています。

開始クラスタから順に次に繋がる使用クラスタをたどっていき、最後尾のクラスタに到達したときには、
『クラスタ中何セクタを使っているか』を記すことになっています。これによりオーバーランせずに済みます。
1 クラスタ = 8 セクタなので 1〜8 なのですが、クラスタ番号と間違えないように最後尾マーク 0xC0 と加算されます。
クラスタ 0x23 から始まった SAMPLE は FAT を見ると 0x23->0x22->0x21->0x20->0x1F->0xC4 で合計 5クラスタと最後に 4 セクタ使って終了と分かります。

もし 256byte 以内で収まるファイルがあるとすると、FAT の該当クラスタには 0xC1 だけが記されることになります。図中の YAHOO がそれにあたります。
その場合でも管理単位はあくまでクラスタなので 8 セクタ中残りの 7 セクタは無駄になってしまいます。そこは利便性とのトレードオフということで。

というわけでまとめ。
読み込みは以上の通りです。
書き込む際は、ディレクトリに名前を登録し空いているクラスタ番号を得て FAT を埋めていけば良し。
消す際はディレクトリの名前部分に 0x00 を書いて、読み込み同様クラスタをたどって 0xFF で未使用に戻していけばOK
書いたり消したりを繰り返すとデフラグが必要になる様が想像できれば完璧です。

FAT はセクタ 14-16 の 3 セクタにまったく同じものが 3 つ書き込まれています。
重要な情報なのでバックアップ必須というわけです。

同梱の D88TOOL では BASIC プログラムの書き込みに対応しましたので、ディスクを使った自作プログラムの制作にも使えると思います。


■ はんてい!!

ようやくこの話…。


ワークは上の図のようになっており、順番に上から下まで、ある場所を基準として右の図のような 4通りのチェックを行います。
チェックを行って同じ石番号=揃っていると判定されたら「揃っているフラグ」を各ポジションに立てておきます。

いま、仮に 3 と 12 と 21 が同じ色だったとします。
ひょっとすると 30 も同じ色かもしれませんが(4連消し)それは 12 を基準に調べたときに判別できます。
3連消しと4連・5連消しでスコアも違いますが、5連消し=3連消しが2つ重なった状態といえるので、
3連消しを発見する度に変数を加算していけば、何連消しなのか分かります。

6 を調べるときに、右側を調べるとフィールドをはみ出しそうな気もしますが、
それを防ぐために 0 と 7 の列に石番号とは全く関係のない値をあらかじめ入れておきます。
右方向のチェックでは 6, 7, 8 と無駄なチェックになりますが絶対に揃わないようになっているのです。
0,7列はどちらか一つでも良いですね。一応外周という名目です。

同様に、最下行で下方向を調べる時も下の2行に石番号と全く関係ない値をいれてあるので判定ミスはおこりません。

あとはフラグに従って、消去→落下を行い、再度判定で連鎖チェック、といった具合です。

■ ASはつらいよ

アセンブラの話が少し出ていたので。

db 2048 dup (0)		; error

db 1024 dup (0)		; ok	
db 1024 dup (0)
他のアセンブラで ds に相当する RAM エリア確保のコマンドですが、1024を超えるとエラーになります。

db 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20	;error
db 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19		;ok		
データを記述する場合、20 個を超えるとエラーになります。
同様に、マクロの変数も 20個を超えると多すぎるとエラー。カンマで区切る指定全般?

ld  a,1+2*-3	;error	
ld  a,1-2*3	;ok
上の場合、 a+-6 と解釈されてしまうようです。なんだそりゃ。

val set 1		;error	
val := 1		;ok
マニュアルには set が使えるように書いてありますが、使えません。:=を使いましょう。

val := 1
Sub:
 jr  $$branch	
val := val + 1
$$branch:
 ld  a,val
エラーになります。$$branch が Sub のローカルシンボルと認識されないからです。
行頭から記述しなければならない変数設定が邪魔になって、ローカルシンボルが一旦リセットされてしまうようです。
意味不明すぎる。

Sub: macro v_1	;error	
 ld a,v_1
 endm

Sub: macro v1	;ok
 ld a,v1
 endm
マクロの変数に「_」アンダーバーを含めると、エラーになります。



■ 反 省

デモを作りたかった。未判明の仕様で実装していない部分をなんとかしたかった。

コツとしてはナナメに揃えること。バクチ連鎖の確率が上がります。
あとは常に次の石を意識すること、かな。

作業する端から新しい仕様が判明したり、必要かどうかも不明な新機能を追加したり、
計画通りにいかないのが常とはいえ、パッチワークの塊の様な出来でした。

P6に関して言えば、せっかくの多色環境なのにパレット機能が無いのが致命的ですね。
解像度がアレなのは許せるとして、パレットがあればVRAM1枚で足りる部分もあるだろうし
2枚使ってスプライト/BGの真似事とか色々できたと思うんですけどねぇ。

勉強不足で 66 のディスク対応が出来なかったのも悔やまれます。
機種判別ルーチンを付けて SR/非SR や 音源の対応状況を取得できるようにしようという計画も
あったのですが I/O ポートの $73(外付けFM) と $A3(内蔵FM) で帰ってくる値が違うようで
うまくいきませんでした。ROMを調べる以外の方法で実現できればと思っていたのですが…。

演出方面に力を入れまくった感はありますが、落ちモノは P6 のパワーでも十分出来そうな実感を得ました。
あからさまに要スプライトなものは無理ですが、ルールを再現するだけならなんとでもなりそう。
落ちモノでなくても、固定画面なら漢字ROMを生かした、もじ○ったんとか面白いかも。

面倒くさいという言葉を口にしそうになる度にぐっとこらえて5ヶ月くらい弄ってたことに。
P6でなく筆者自身のやる気ベンチマークだったのか、と巧いオチが付いたところでお開き。

special thanks to...
Hashiさん : ほとんど全てのグラフィックを提供して頂きました。
のりさん : キーボード入力周りのコードを提供して頂きました。
Exomizer ソースコードを使用させて頂きました。

▲ TOP