読者です 読者をやめる 読者になる 読者になる

MSX-BASIC の1画面プログラミング技術

この記事は MSX Advent Calendar 2015 の6日目の記事です。

1980年代に MSX・FAN という雑誌にファンダムという MSX-BASIC プログラム投稿コーナーがあり、私も投稿して腕を磨いていたものです。で、ファンダムでは1画面プログラム部門、N画面プログラム部門、10画面プログラム部門、というようにプログラムの長さに応じてカテゴリ分けがありました。

ここで1画面というのは、MSX のスクリーンモード0、つまり幅40文字、高さ24文字のテキスト表示で1画面に収まる分量のコードのことです。当然ながら短いコードで多くの機能や面白さを実現できれば評価が高まるわけで、自然と1画面プログラム部門などは極限までのコード削減テクニックが競われておりました。それは、スパゲティプログラムの奨励事例 - スパゲティプログラム - Wikipedia に載ってしまうくらいに。

当時私は中学生でしたが、人をあっと言わせたい一心でわけもわからぬままコード圧縮技術の探求に邁進したものです。本記事では、当時から伝わる伝統芸能の保存のため(笑)、その数々のテクニックを記録しておこうと思います。

初級

変数名を1文字にする

まあ一番初めに思いつきますね。これはその後 iアプリ(携帯向け Java)が登場した時、10KBという制限の中にいかにプログラムを詰め込むかという課題に対し、1文字クラス名、1文字メソッド名を使用するというテクニックに引き継がれるわけです(笑)。

行番号を1文字にする

BASIC のプログラムは伝統的に 10 から始まって 20, 30, ... と増やしていくものですが、行番号に2文字使うのは無駄なので1文字に。

空白は全て削除

例えば FOR 分は「FOR I = 0 TO 100」と書くのが可読性が高いコードでしょうが、ここは黙って「FORI=0TO100」。FOR と 変数 I がくっついて心配になりますが、FORI という命令はないので全然平気。MSX-BASIC では空白はすべて削除することが可能です。ただし「X OR Y」というようなコードを「XORY」としてしまうと予約語の「XOR」と解釈されてエラーとなってしまう罠もあるので注意です。

演算子の優先順位を把握して括弧を削減

加算乗除だけでなく、関係演算子、論理演算子、剰余など、演算子の優先順位を正確に把握すれば不要な括弧を取り除くことができます。

リプレイはRUN

ゲームなどで始めからやり直す場合の処理は面倒なので、単純に RUN を実行。

中級

1行の文字数を40文字の倍数に近づける

行の途中で改行してしまうとその後の空白の分が無駄になってしまうので、画面の右端まで使い切るように命令をコロン(:)でつなげてコードを埋めます。さらに、改行してしまうと行番号+空白の2文字分も無駄になるので、ジャンプ命令などが不要であれば、1行の上限255文字に一番近い40の倍数である240文字に近づけておきたいところです。

比較演算子の結果を制御または計算に利用

MSX-BASIC では比較演算子の結果は真なら -1、偽なら 0 が返るので例えば

IF A > 5 THEN B = 0 ELSE B = 12

なら次のように書き換えることが可能です。

B=-(A<=5)*12

また、条件付きループのコードは次のように書けます。

FORI=0TO1:(何かの処理):I=-(A=1):NEXT

スプライトパターンの定義

スプライトの定義は普通

10 FOR I = 0 TO 7
20 A$ = A$ + CHR$(VAL("&H" + MID$("21D4D4D427B2AAB3", I*2 + 1, 2)))
30 NEXT
40 SPRITE$(0) = A$

なんて書くと、ビットパターンを16進数で直感的に表せるので便利なわけです。あ、現代プログラマはあまり慣れていないかもしれませんが、8ビットマシンプログラマはビットパターンを高速に16進数に置き換えることができるのです(笑)。ところが1画面プログラムでは文字数がもったいないのでこうです。たまたまビットパターンの文字が制御文字になってしまうと使えないのが難点ですが。

SPRITE$(0)="!ヤヤヤ'イェウ"

NEXTの変数の省略

NEXTの対象のループ変数は省略。ただし2重ループを閉じる時はNEXTJ,Iのようにまとめると良い場合も。

キーの入力待ち

10 IF STRIG(0) = 0 THEN GOTO 10

によってキーの入力待ちが実現できますが、1行を消費してしまうのがもったいないので次のように行の途中に書けるようにします。

FORI=0TO1:I=-STRIG(0):NEXT

LINE ではなく DRAW

グラフィックモード(SCREEN 2 など)で垂直、水平、斜め45度の直線などを描画する場合は、LINE を使いよりも DRAW を使うことでコードが短くなる場合があります。

LINE(80,100)-(180,100),15

例えば上記のコードは下記のように書き直せます。

DRAW"S4BM80,100C15R100"

命令の省略形を使う

例えば PUT SPRITE 命令の定義は次の通りです。

PUT SRPITE <スプライト面番号>[,{(X,Y)|STEP(X,Y)}[,<色>[,<スプライトパターン番号>]]]

あるスプライトの色を変えるだけであれば座標は省略可能なので、次のように短縮できます。

PUTSPRITE0,,8

上級

ストライプ操作にVPOKEを使う

スプライトを消すために、例えば画面外の位置にスプライト表示を行うコードは次のような感じです(スクリーンモード1〜3の場合)。

PUT SPRITE 0, (0, 192)

が、VPOKE を使って VRAM の内容を直接書き換えることでコードを短くすることができます。スプライト#0 の Y座標を格納するVRAMアドレスは 6912 番地なので、こんな感じ。

VPOKE6912,192

この他にも、画面表示関係は VPOKE の使用により効率的なコードになるケースが多く、応用が効きます。

エスケープシーケンスの利用

スクリーンモード1などで任意の文字座標に文字列を表示する場合のコードはこんな感じです。

10 LOCATE 8,10
20 PRINT"PRESS ANY KEY"

これはエスケープシーケンス CHR$(27) + "Y" + CHR$(32 + y) + CHR$(32 + x) を用いてこう書けます。多用する場合は E$ = CHR$(27) のように変数に格納しておけばさらに短くなるでしょう

PRINTCHR$(27)+"Y*(PRESS ANY KEY"

表示する文字が1文字だけであれば、上記の VPOKE で VRAM を直接書き換える方法も効果的です。

変態級

マシン語直書き

BASIC プログラムにもかかわらず、コメント部分に直接バイナリのマシン語コードを記述するという荒技です。BASIC のコードはマシン語コードをロードする必要最小限のみ。まったく理解することが不可能です。しかしながら、比較的大規模なコードを非常に高速に実行できるため、1画面プログラムとは思えないほどの挙動を示します。下記のコードは、BASIC プログラムがメモリの &H8000 番地から格納され、最初のコメント(' 以降)の位置のバイナリが &H801C 番地に来ることを見越して、USR 関数のアドレスを書き換えて直接呼び出しています。

1 CLEAR9,&HD000:DEFUSR=&H801C:A=USR(0)'ン!、♠!0ハッGゆ>7ヨ0W7まク9#ェ...

コーディングゼロでエンディングメッセージを実現

私が昔「8192階建ての塔」という縦スクロールジャンプアクションゲームを作ったときの話。塔を登っていくとどこまで進んだかをカウントしている変数が増え、プレーヤーはそれをスコアとして競います。サイズに余裕がないのでゴールに到達したときの画面表示もできません。が、カウント変数が32767を超えたとき(階数は4で割った値なので8192階に達したとき)に Illegal function call エラーが起きることに気づきました。ならばそれをエンディングとして使用すればよい、ということでタイトルが決定しました。なんつって。

おまけ

参考までに1画面プログラム「8192階建ての塔」のコードを載せておきます。40x24文字びっしり埋まってます。

f:id:nagixx:20151206042512g:plain

1 SCREEN1,1,0:COLOR15,0,0:WIDTH24:KEYOFF
:DEFINTA-Z:DEFFNA=VPEEK(6208+X¥8+(Y¥8)*3
2)ORVPEEK(6209+(X+7)¥8+(Y¥8)*32):FORI=1T
O32:VPOKE14335+I,ASC(MID$("jjョ-kェ}まjj.jェ
jFfjj/-はl アjj.jljFJ",I,1))-46AND255:NEXT
2 A=RND(-9):VPOKE8208,50:FORI=0TO20:VPOK
E6211+i*32,133:VPOKE6236+I*32,133:NEXT:F
ORI=0TO9:LOCATERND(1)*19,2+I*2:PRINT"ooo
ooo":NEXT:LOCATE0,22:PRINTSTRING$(24,133
);:X=32:Y=160:A=1:F=1:SOUND1,0:SOUND7,62
3 X=X+A*4-2:P=PXOR1:GOSUB6:IFFNA=32THENY
=Y+8:SOUND8,0:GOTO3ELSEB=S:S=STRIG(0):IF
STHENJ=JMOD12+1:SOUND0,99-J:SOUND8,15:GO
TO3ELSESOUND8,0:IFB=0ANDS=0THENJ=0:GOTO3
4 X=X+A*4-2:Y=Y-J:J=J-1:GOSUB6:IFJ<0ANDF
NA<>32THENJ=0:Y=(Y¥8)*8:GOTO3ELSEIFY>64T
HEN4ELSEF=F+1:FORI=0TO3:C=C+1:LOCATE0,2:
PRINTCHR$(27)+"L":Y=Y+8:IFCMOD2=0THENLOC
ATERND(1)*9+(C/2MOD2)*10,2:PRINT"oooooo"
5 GOSUB6:NEXT:LOCATE10,0:PRINTF"F":GOTO4
6 PUTSPRITE0,(X,Y),7,A*2+P:A=AXOR-(X=32O
RX=208):IFY<176THENRETURNELSEPLAY"SM999L
64DD-C":FORI=0TO20:LOCATE,2:PRINTCHR$(27
)+"M":NEXT:FORI=0TO1:I=STICK(0):NEXT:RUN