プログラミング小ネタ集
PB-100のBASICは数々の制約が厳しいですが、その中にいかに多くの要素を詰め込むかにパズル的な面白さがあり、先人により多くのノウハウが開発されてきています。それらも含めて、NUAOがゲーム作成において見つけたり利用させていただいた小ネタについて、ここに書き連ねて随時更新していきます。こんなのもあるよ!とか、これは間違っているよ!とかあればDMでお知らせください。
容量削減ネタ
PB-100は標準で544steps、拡張しても1,568stepsという極めて少ない容量しかありません。だからこそ、1stepをひねり出すために試行錯誤を繰り返す必要があります。どなたかが表現されたように、まさしく「俳句」です。
SGN関数を使って条件分岐を短縮
変数をカウントアップするが、ある上限で頭打ちにしたい場合、
A=A+1:IF A>9;A=9.....14steps
IF A<9;A=A+1.....10steps
などとするのが普通ですが、SGN関数を使えば
A=A+SGN(9-A.....9steps
に削減できます。関数なら閉じカッコを省略できることも魅力です。
IF文の自由度が少ない(論理演算ができない、ELSEが使えない)こともあり、このようにスマートに関数にしたいところですが、PB-100のIF文は意外と速いため、容量と速度の背反で悩むことが多いです。
$のフル活用
PB-100では文字列変数も使えますが「A$」と書くだけで2stepsを消費します。例えばキースキャン後の処理などで文字列変数を何度も参照する場合は「$」を使うと容量を削減できます。但し、$はMID関数に使える唯一の貴重な変数なので、一時退避・復帰が必要かもしれません。
10 A$=KEY:IF A$="" THEN 10
20 IF A$="4";×××
30 IF A$="6";×××
↓
10 $=KEY:IF $="" THEN 10
20 IF $="4";×××
30 IF $="6";×××
配列変数の1文字変数への置き換え
PBで配列変数も使えますが、括弧を含めるとstep消費はバカにできません。同じ配列変数を何度も操作する場合は、最初にいったん1文字変数に置き換え、最後に書き戻すテクニックがあります。
A(I)=A(I)+2*A(I)+INT(A(I))*A(I)
IF A(I)>0;IF A(I)<9 THEN 1
A(I)=A(I)^2
.....57steps
↓
B=A(I)
B=B+2*B+INT(B)*B
IF B>0;IF B<9 THEN 1
B=B^2
A(I)=B
.....42steps
配列変数のオフセット
PB-100の変数・配列変数は、例えばC、C(0)、B(1)、A(2)がどれも同じ変数を指すという特異なものです。逆にこの性質を利用すれば、配列変数に対するアクセス記述を短縮できます。但しリストは若干読みにくくなります。
A(J)=A(J+1)+A(J+2).....18steps
↓
A(J)=B(J)+C(J).....14steps
ウェイトタイマー
PB-100には一定時間処理をホールドする命令がありません。表示を読ませるシーンなどウェイトタイマーが欲しい場面は多いので、極力少ないstepで実現すべく、いろいろ考えられています。()内に、PB-100実機で各ウェイト式をFOR~NEXTで100回反復したときの実測所要時間を示します。
A=π^π^π (27sec)
A=RAN#^RAN#^RAN# (32sec)
A=SIN SIN SIN SIN 1 (40sec)
A=TAN TAN TAN TAN 1 (25sec)
FOR J=1 TO 50:NEXT J (38sec) .....ループ終了値により可変タイマーとなる
ゲーム「ENEMY」では「重いサブルーチンを空打ちする」というアイデアを思いつきました。(変数に影響がないことが条件)
他にもアイデアをお持ちの方は、ぜひお知らせください。
変数に応じて表示キャラを切り替える
変数Aの内容に応じて表示キャラを切り替える場合、
IF A=0;P$="A"
IF A=1;P$="B"
IF A=2;P$="C"
PRINT P$
.....36steps
などとやる人はいないと思いますが、PB-100では
$="ABC"
PRINT MID(A,1)
.....14steps
とやるのが一般的です。RPGなどで、敵キャラや背景キャラの全てをあらかじめ$に設定しておけば、後は数式さえ工夫すればMID関数で所望のキャラクタを指定できます。
小粒ネタ
・関数の閉じカッコ省略・・・PBシリーズでは有名な削減テクニックです。FRAC(A/10)*10 の場合は 10*FRAC(A/10 と書けば閉じカッコが省略できます。ただ、閉じカッコの省略は場合によってはエラーが出る場合もあるようなので、テストで問題ないことを確認した方がよいです。少なくともMID関数は閉じカッコ省略不可です。パラメータが2つあるからでしょう。
・計算優先順位・・・PB-100の計算優先順位は「関数>べき乗>乗除>加減」と決まっているので、式の順序を入れ替えて何とか括弧を省略しましょう。
・マルチステートメント・・・複数行に亘る記述はマルチステートメント化でstep削減できます。2行を1行にすれば、2stepsの削減になります。但し一行の最大文字数制限と、2行目がどこからかのジャンプ先でないかに注意が必要です。
・行番号の圧縮・・・プログラム開発の終盤(もう大きな変更がない)にて、行番号を圧縮して桁数を減らします。行番号自体は桁数に関係なく2steps固定ですが、ジャンプ先の行番号指定は桁数分消費するので、効果が大きいです。
・定数の桁削減・・・例えば時間待ちで100回ループする場合、99回でも結果に大差ありませんが、1step削減できます。
FOR I=1 TO 100:NEXT I → FOR I=1 TO 99:NEXT I
・-1<n<+1の乱数生成
RAN#*2-1.....5steps
RAN#-RAN#.....3steps(但し、得られる乱数の分布は0近くが多くなる)
・CSR、MID、GOTO、GOSUBの引数の整数化省略・・・PB-100BASICの面白い特徴ですが、これら命令の引数は小数であっても勝手に整数化(INT)してくれるので、INT命令は省略できます。ちょっぴり速度改善にもなるはずです。
速度向上ネタ
PB-100の計算速度は60FLOPS程度しかありません。少しでも速度向上を図るべく、ストップウォッチを手に試行錯誤することもまた、PB-100プログラミングの味わいといえるでしょう。
べき乗より乗算
乗算に置き換えられるべき乗は置き換えましょう。圧倒的に速いです。
A=B^2.....14sec(100回反復)
A=B*B.....2sec(100回反復)
乗算が増えると差は縮みますが、5乗ぐらいでも乗算の方が速い。step数との背反ですが。
A=B^5.....14sec(100回反復)
A=B*B*B*B*B.....5.5sec(100回反復)
頻度の高いサブルーチンは若い行番号に
これは先人の門真なむ様により確認された技法です。PBのBASICシステムは、ジャンプ先の行番号を、毎回毎回、先頭から順に一文字一文字順に探す動きをするらしいです。従って、ある行へジャンプする場合は、その行番号より前のプログラムボリュームをなるべく少なくする方が速い、つまり、ジャンプ先はできるだけプログラムの先頭にあるほうがよいということです。但し、GOSUB~RETURNや、FOR~NEXT、他のプログラムバンクへのジャンプなどは、どうもテーブルを持っているらしく、どこにあっても速度は変わらないそうです。(2024/4/21追記修正)
変数削減ネタ
PB-100で使える変数は、基本的にA~Zの26個と、$の計27個です。DEFMコマンドで変数を拡張すれば増えますが、step数を爆食いするので、できれば基本変数内で収めたいところです。
FOR~NEXTのカウンタと初期値の共用
NUAOは最初抵抗があったのですが、FOR~NEXTのカウンタと初期値は兼用できます。例えば、可変長ウェイトタイマーを
FOR A=0 TO B:NEXT A:RETURN
などと記述し、Bにウェイト長を指定してCALLする場合があると思います。この場合AとBの二つの変数を消費してしまいますが、これを
FOR B=B TO 0 STEP -1:NEXT B:RETURN
と記述すると変数はBだけで済みます。つまり初期値のBはループカウンタBと共用できます。但し当然ながらBの値は保存されません。ちなみに、ループカウンタと終了値を共用にすると、エラーにはならないもののPBが沈黙する面白い状態になります。終了値がループカウンタと一緒に逃げて行ってしまうのでしょうか。
FOR B=0 TO B :NEXT B:RETURN.....NG?
変数の符号に意味を持たせる
Rez Finite PB+では、ウィルスの接近状態を配列変数P(*)に格納していますが、ロックオンしているかしていないかで画面表示キャラを切り替える必要があります。そこで、P(*)の符号にその情報を持たせることにし、使用変数の数を抑えています。(未ロックオン=正、ロックオン=負)
50 E$=KEY:IF E$="" THEN 80
55 P(C)=-P(C):IF E$<>"3" THEN 70
50行でキーが押されていたら、55行でP(*)の符号を反転(ロックオン⇔未ロックオン)しています。
変数の小数部と整数部に別の情報を持たせる
Adieu GE999では、次のポイントが右左折どちら用かということと、現在の状態(直進/右左折)を、併せて変数Vに格納しています。
V=0.1:次ポイントは右折用/現在は直進状態
V=1:次ポイントは右折用/現在は右折状態
V=0.2:次ポイントは左折用/現在は直進状態
V=2:次ポイントは左折用/現在は左折状態
こうしておくと、まずINT(V)で表示を0=←、1=↑、2=↓で切り替えできます。次に、直進状態から右(左)折状態に切り替える場合は10倍、逆は1/10倍すれば、右(左)折用の情報を保存したまま、直進状態と右(左)折状態を切り替えることができます。
またXANADU PBでは、各部屋の状態(モンスター位置)を配列変数Z(*)に保存していますが、モンスターを倒して宝箱になった場合の宝箱の位置はZ(*)の小数部に保存するようにしています。Z(*)が整数ならモンスターが生きていてその位置を、小数なら宝箱の位置を、ゼロならなにもなし、となります。
PB-100及びシミュレータのクセ回避ネタ
PB-100のBASICは、標準的なBASICに比べて制約やクセが多いような気がします。後継機種では改善されたり拡張されていることもあるようですが、なぜかPB-100コミュニティでは最初期のヘンなBASICのお作法に則ることが多いです。最も対応機種が広いからというのもありますが、多い制約やクセを克服することを面白がっているフシもあるようです。
VAL()関数の中にMID()を入れ子にするとエラーになる
VAL()関数の中は単純文字変数だけです。配列文字変数も、”A"といった指定も、MID関数も使えません。従って、いったん別の単純文字変数に格納してからVAL()に代入する必要があります。ただ、PB-SIMはOKみたいです。
PB-SIM:
B=VAL(MID(2,1)).....OK
実機、Pokecom GO:
B=VAL(MID(2,1)).....NG.
A$=MID(2,1):B=VAL(A$).....OK
KEY関数のヘンなクセ?(実機)
KEY関数は瞬間に押されているキーを文字列として読み取りますので、数字として読みたい場合は
A=VAL(KEY).....NG
としたいところですが、VAL()関数の制限からエラーが出ます。そこで、
B$=KEY:A=VAL(B$).....NG
としますがやはりだめです。いろいろ試行錯誤して、結局、
B$=KEY:B$="0"+B$:A=VAL(B$).....OK
としなければ通りません。PB-100ではB$=""(キー入力無し)の場合に、Bは数値変数(B=0)として扱われ、VAL関数で拒否されるからでしょう。ちなみに、以下もダメです。
B$="0"+KEY:A=VAL(B$)....NG
B$=KEY:A=VAL("0"+B$).....NG
確かに、取説にはKEY関数の書式として「文字変数=KEY」とだけしか書いていないので、仕様と言われればその通りですが・・・。ただし、
IF KEY="1" THEN ***.....OK
という書き方は可能です。
PB-SIMのFRAC()関数に大きな桁数落ちがある
PB-SIMのFRAC()関数には比較的大きな桁落ちがあります。誤差が大きくなって計算結果が想定からズレることがあるので、注意が必要です。(実機、Pokecom GOでは問題なし)
実機、Pokecom GO:
FRAC(1/3)=0.33333333333
FRAC(1/12345678)*12345678=1.....OK!
PB-SIM:
FRAC(1/3)=0.333333
FRAC(1/12345678)*12345678=0.....NG...
PB-SIMではIF文中の条件判断式が複雑になると強制終了する?
IF I*J*(I-L+1)*(J-L+1)=0;***.....ERR
A=I*J*(I-L+1):A=A*(J-L+1):IF A=0;***.....OK
STEP数がかさみ変数消費もあるので、残念なところです。(実機、Pokecom GOでは問題なし)