HSPコンテストショート部門向け資料

これは?

年1回行われるHSPコンテストのショート部門向けの資料です。

この資料は05年度に行われた「HSPコンテスト - ショート部門」で得た経験を元にしたもので あって、それ以降に行われるコンテストの内容にあわない内容もあるかもしれません。

なお、この資料が対象としているHSPのバージョンは 3.0 です。

始めに

まず、考えなければいけないのはコンテストに何を出すかでしょう。(当たり前ですが)

で、ここで注意していただきたいのは、このコンテストは「プログラムをどのように 詰め込むか」ではなく「制限された状況でどれだけのものを表現できるか」 が重要です。いまから紹介する方法は、詰め込む方法ではなく 「これを表現したい」という願望をかなえる「ひとつの手段」でしかないのです。

とはいっても、これはあくまで自分の考えであって一般論とは違うかもしれません。 あと自分は「プログラムを詰め込む」の方も好きです。

※ 前置きはいいからさっさと実例を見せろというせっかちさんへ、>>応用

ソースと容量

以下のリストはコンテスト出場前に作成したもので、何をするとどれだけの容量を食うか をあらわしています。必要なものだけを調べたので完璧に網羅しているかはわかりません。

0BYTE

括弧、コロン

1BYTE

文字列として参照  (実際に使うときは文字自体のコスト+1になります。 例えば "foobar" なら 6 + 1 (文字列自体のコスト+文字列として参照)で 7 です。)

4BYTE

変数の参照、自然数の定数の参照、変数の配列への参照、命令、代入、複合代入、四則演算、剰余、配列の区切り、文字コードとしての参照(例えば'a'は4byte)、ラベル、インクリメント、デクリメント

6BYTE

負の整数の定数の参照、if、 else

8BYTE

命令、関数の引数の設定、一部の特殊な命令( repeat, loop, continue, break 他にあるかも)、

12BYTE

関数の参照、実数値の参照、 #module (モジュール名は関係ない)と #global 二つあわせて

33BYTE

命令、関数の定義(名前の長さも関係する)

114BYTE

何も書かなくても付加される。

上記補足

注意していただきたいのが、マクロです。例えばforeachは命令なので4BYTEで すみそうに見えますが、実際にはマクロが内部で展開されるので20BYTEになってしまいます。 同様に ginfo_* な命令も内部で展開され関数になるので12BYTEになります。

実例

例えば以下のコードは何バイトになるでしょうか?

    a = 2

これは

対応する部分消費コスト
a変数の参照だから4BYTE
=代入だから4BYTE
2定数の参照だから4BYTE

ですから、4 + 4 + 4 + 114(何も書かなくても付加される) = 126BYTE になります。(なんか見にくくてすみません)

ではこれは?

    a.2 = 2

配列変数と普通の変数のコストは同じだから、上と同じで 126BYTE?違います。答えは

対応する部分消費コスト
a変数の参照だから4BYTE
.変数の配列への参照だから4BYTE
2定数の参照だから4BYTE
=代入だから4BYTE
2定数の参照だから4BYTE

だから 4 + 4 = 8BYTE 余分に食って、126 + 8 = 134BYTEになります。

応用

わざわざ書いたのになんですけど、上のはわざわざ覚えなくていいです。 ですので、ここでわからなくても無理に理解する必要はありません。

というわけでやっと応用に入ります。

以下の例ではBoolean値や NUM_* のように暗黙のうちに使用している定数が多々ありますが、ご了承ください。

#constを使う

    a = FOOBAR_POSX + FOOBAR_WIDTH

のように、定数同士の演算であれば

#const FOOBAR_FOOTX FOOBAR_FOOTX + FOOBAR_WIDTH
    a = FOOBAR_FOOTX

のように変えましょう。

何度も使う文字列を変数に入れる

文字列の使用回数によりますが

    font "MS ゴシック", 32 : mes "ポリンキー、ポリンキー"
    font "MS ゴシック", 24 : mes "三角形の秘密はね"
    font "MS ゴシック", 16 : mes "教えてあげないよ。ジャン"
    font "MS ゴシック",  8 : mes "е#★Wゞポリンキー"

のようにするよりも

    _MSGothic = "MS ゴシック"
    font _MSGothic, 32 : mes "ポリンキー、ポリンキー"
    font _MSGothic, 24 : mes "三角形の秘密はね"
    font _MSGothic, 16 : mes "教えてあげないよ。ジャン"
    font _MSGothic,  8 : mes "е#★Wゞポリンキー"

のようにしたほうがお得です。

マイナスの値を変数にいれる

同じく使用回数によりますが、マイナスの値は変数よりコストが高いので

    mes -1
    mes -1
    mes -1
    mes -1

のようにするよりも

    min1 = -1
    mes min1
    mes min1
    mes min1
    mes min1

のようにしたほうがお得です

dupを使う

やっぱり使用回数によりますが、配列変数は普通の変数よりもコストが高いので、

    mes foobar.cnt * ( foobar.cnt + foobar.cnt ) / foobar.cnt

のようにするよりも

    dup _foobar, foobar.cnt
    mes _foobar * ( _foobar + _foobar ) / _foobar

のが得です。

配列の文字列の初期化を工夫する

配列の定義のカンマは何気にコストがつきますから

    songtitle = "風に向かって", "木漏れ日の日の中で", "幻想の夜曲", "誕生", "揺れる心", ...

のようにするよりも

    tmp = "風に向かって,木漏れ日の日の中で,幻想の夜曲,誕生,揺れる心, ... "

    repeat NUM_SONGS
    getstr songtitle.cnt, tmp, idx, ','
    idx += strsize
    loop

のように一度適当な変数に放り込んでから、getstrでくくりだした方が得になることがあります。 しかしこれは、タイピングソフトのようによほど大量の文字列を使う状況でないと 逆に損します。

引数を省略する

    color 0xff, 0xff, 0

この場合、最後の0は省略できて(大抵は省略すると0が渡される)

    color 0xff, 0xff

とするとお得です。 とはいっても redraw なら省略したとき1が渡されたり、 pos ならカレントポジションが 渡されたりと例外もありますので注意しましょう。(そういえば、関数に変更されたせいか rnd の 引数は省略できなくなってます。2.x時代は何故か 0 ~ 99 の値が返ってきましたが)

複合代入演算子を活用する

複合代入演算子というのは、 +=, -= など演算と代入を一度にやる演算子のことで、これはかなりお得な演算子だったりします。 例えば

    foo = foo + 2

これはもちろん

    foo += 2

としたほうが、お得です。

また、いつの間にかほぼ全ての演算子に複合代入演算子が実装されていて

    foo /= 2

のような記述も可能だったりします。

複合代入演算子をもっと活用する

もっと活用するといってもそんな大それたものではありませんが、例えば

    foo = -foo

これは

    foo *= -1

と書き直せます。(これは下の「負の値の定数を考える」と組み合わせないと、意味ないですが)

さらに

    flag = ( flag == FALSE )

    flag = 1 - flag

のように一回実行するごとに、真になったり偽になったりするおなじみの式も

    flag ^= TRUE

と書き直せます。

負の値の定数を考える

プログラムを作っていて負の定数を使いたくなることがあるでしょう。こんなとき

#const FOOBAR -2

とするよりも

    FOOBAR -= 2

のように、ファイルの上の方に書いておいたほうが得することがあります。 (やっぱり使用回数に依存します)

特に -1 を使用したいときは

    MIN1 -= 1

としてもいいですが、もっと工夫して

    MIN--

としたほうが、よりお得です。

注意していただきたいのが、この変数への代入操作の回数です。当たり前ですが、この場所を2回も3回も通ってしまったり、対象の行よりも前で既に0以外で 初期化されていると台無しになってしまいます。

n次方程式を小さくする

n次方程式とは

    y = 2 * x * x + 3 * x + 4

みたいな感じの特定の項がn乗になってる方程式のことで(これは方程式とはちょっとちがう?)、これは因数 "x" でくくって

    y = x * ( 3 + x * 2 ) + 4

のように書き直すことができます。これは演算子が括弧に変わったことで若干得です。

同様に因数 "x" をネストすることで

    y = 5 * x * x * x * x + 4 * x * x * x + 3 * x * x + 2 * x + 1

みたいなのも(こんなのどう考えても使いませんが)

    y = x * ( 2 + x * ( 3 + x * ( 4 + x * ( 5 ) ) ) ) + 1

のようにするとだいぶ得です。

ifを考える

例えば、以下のようなことをやっていないでしょうか

    if( ( value < 20 ) == FALSE ) {
        dosomething
    }

当然ながらいつもの癖で安易に否定を取ると損してしまいます。 こう直しましょう。

    if( value >= 20 ) {     // 小なり記号が大なり記号になっています。
        dosomething
    }

ただこれは、if ステートメントが本来表現しようとしていたものを、破壊してしまいますのである程度完成したあとにやるべきです。実際にやるのであれば

    // if( value < 20 ) == FALSE ) {        // <- original
    if( value >= 20 ) {
        dosomething
    }

みたいに元のを残しておくといいでしょう。

まぁ今までのは実践している人も多いでしょう。 物足りないという人のために、ちょっと意外な事実を公開します。

    if( flag == FALSE ) {
        dosomething
    }

これは一見すると小さくならなそうに見えます。 でも実は、こうするとしっかり縮んじゃいます。

    if( flag ) : else {
        dosomething
    }

これは案外気づいた人も少ないんじゃないでしょうか?僅かながら小さくなります。 どうです、意外でしょう?え、知ってる?あ、そうですか・・・

ifをもっと考える

ドモルガンの式を使うと

    if( flag1 == FALSE & flag2 == FALSE ) {
        dosomething
    }

これは

    if( ( flag1 | flag2 ) == FALSE ) {
        dosomething
    }

と書き直せます。同様に

    if( flag1 == FALSE | flag2 == FALSE ) {
        dosomething
    }

    if( ( flag1 & flag2 ) == FALSE ) {
        dosomething
    }

になります。

実際には上の「ifを...」を使用して 前者を

    if( flag1 | flag2 ) : else {
        dosomething
    }

後者を

    if( flag1 & flag2 ) : else {
        dosomething
    }

にすることが可能です。

真偽値を工夫する

真偽値で値を初期化しようとするとき

    flag1 = TRUE
    flag2 = FALSE

のようにしていませんか?これは実は

    flag1++
    dim flag2

これでも大体同じ意味になります。(もちろん厳密に言えば、異なった意味をあらわしています。 特に flag1++ の方は 2 ** 32 回連発すると多分おかしくなるので - 実際にはありえませんが - 普通のソフトを作るときには使用すべきではありません。)

また配列であれば

    flag.1 = TRUE
    flag.2 = FALSE

    flag.1++
    poke flag.2

で大体同じ意味になります。ただこっちは、大会終了後に気づいたので公開しているスクリプトには使用されていません。

memcpyを活用する

数値型の配列をコピーするとき

    repeat NUM_ARRAY
    toArray.cnt = fromArray.cnt
    loop

のようにしていませんか?これは memcpyを使って

#const SIZEOF_INT   4
#const ARRAY_SIZE   NUM_ARRAY * SIZEOF_INT
    memcpy toArray, fromArray, ARRAY_SIZE

のようにしたほうがいいです。( SIZEOF_INT は整数型の変数のサイズ )

また、このコードなら大部分がネイティブコードで実行されるので速度面でも有利です。

memsetを活用する

配列を初期化するときまれに memsetが有効になることがあります。例えば

    repeat NUM_ARRAY
    foobar.cnt = 0
    loop

これは

#const SIZEOF_INT   4
#const ARRAY_SIZE   NUM_ARRAY * SIZEOF_INT
    memset foobar, 0, ARRAY_SIZE

のようにしたほうがいいです。( SIZEOF_INT は整数型の変数のサイズ )

例によって、大部分がネイティブコードで実行されるので速度面でも有利です。

かなりまれな状況かもしれませんが、全てを -1 で初期化したい場合は

#const SIZEOF_INT   4
#const ARRAY_SIZE   NUM_ARRAY * SIZEOF_INT
    memset foobar, 0xff, ARRAY_SIZE

なんてこともできますし、全てを「ある程度大きい数」で初期化したいときは

#const SIZEOF_INT   4
#const ARRAY_SIZE   NUM_ARRAY * SIZEOF_INT
    memset foobar, 1, ARRAY_SIZE

なんて使い方もできます。

関数を定義してみる

この方法が役に立つことはまれであんまり使い道は無いかもしれませんが、 一応紹介しておきます。以下は色の使い方を工夫することで、微妙にコスト 削減を試みた例です。

    color 0xff, 0xff, 0xff
    mes "1"
    color 0xaa, 0xaa, 0xaa
    mes "2"
    color 0x11, 0x11, 0x11
    mes "3"
    color 0x55, 0x55, 0x55
    mes "ちゃっらーん"
    ...
    ...

このように色の指定をするとき R, G, B それぞれに同じ値を指定するのであれば 少し無駄な感じに見えます。そこで、以下のように変更してみます。

#module
#deffunc SepiaColor int c
    color c, c, c
    return
#global
    SepiaColor 0xff
    mes "1"
    SepiaColor 0xaa
    mes "2"
    SepiaColor 0x11
    mes "3"
    SepiaColor 0x55
    mes "ちゃっらーん"
    ...
    ...

何度も使えばお得です。ただこんなことやっても表現の自由度が減ったわりに対して報われないのでお勧めしません。

モジュールを定義しないで関数を定義してみる

上の例ですがモジュールの部分を微妙に変更して

if 0 {
#deffunc SepiaColor int c
    color c, c, c
    return
}

としても動きますし微妙に得です。といってもこのような方法はHSP製作者の側からしてもおそらく想定外でしょうから、あまり使わないほうがいいかもしれません。

サブルーチンを使用する

何度も同じ処理をするのであれば、サブルーチンを使ったほうがいいです。

    foo = 1
    bar += 5
    hoge = bar * 2
    piyo = bar / hoge

    ...
    ...

    foo = 1
    bar += 5
    hoge = bar * 2
    piyo = bar / hoge

これはもちろん

    gosub*calculate
    ...
    ...
    gosub*calculate


*calculate
    foo = 1
    bar += 5
    hoge = bar * 2
    piyo = bar / hoge

にしたほうが得です。

範囲指定を工夫する

整数の定数を使った範囲指定は工夫すれば小さくなる場合があります。 このとき対象になる変数も整数型で宣言されていなければなりません。

    if( ( FROM_LIMIT <= foobar ) && ( foobar <= TO_LIMIT ) ) {
        dosomething
    }

これは以下のように書き直すことができます。

#const AVG_LIMIT ( TO_LIMIT + FROM_LIMIT ) / 2
#const DIF_LIMIT_HALF ( TO_LIMIT - FROM_LIMIT ) / 2
    if( ( foobar - AVG_LIMIT ) / DIF_LIMIT_HALF ) : else {
        dosomething
    }

特に FROM_LIMIT =- TO_LIMIT なら AVG_LIMIT = 0 になりますから 省略してしまって

#const AVG_LIMIT ( TO_LIMIT + FROM_LIMIT ) / 2
    if( foobar / DIF_LIMIT_HALF ) : else {
        dosomething
    }

この方法はブロック崩しなどで、動かない物体との当たり判定をしたいときに有効です

もちろん範囲指定が逆になったときも同じような方法は使えます。

    if( ( foobar < FROM_LIMIT ) && ( TO_LIMIT < foobar ) ) {        // 変数名がちょっと不適
        dosomething
    }

これは

#const AVG_LIMIT ( TO_LIMIT + FROM_LIMIT ) / 2
#const DIF_LIMIT_HALF ( TO_LIMIT - FROM_LIMIT ) / 2
    if( ( foobar - AVG_LIMIT ) / DIF_LIMIT_HALF ) {
        dosomething
    }

のように else での否定を抜くだけで動きます。 もちろん FROM_LIMIT =- TO_LIMIT なときも同様に else を抜くだけで動きます。

文字を点滅させる

作品を作るうえで、ある文字を目立たせたいということがあるでしょう。

そんな時に、わりと低コストかつ効果的なものとして点滅をおすすめします。 例えば文字を点滅させるとしたら直感的には以下のようなものが思いつくでしょう。 簡単のためにキャプションバーを使います。

    repeat
    if( cnt \ 2 == 0 ) {
        title "ドンタコスったら"
    } else {
        title "ドンタコス"
    }
    await 500
    loop

ただこの方法ではちょっと欠点があります。 というのは最後の await 500 のせいで他の処理も一緒に遅くなってしまうのです。 点滅専用のアプリケーションならそれもありかもしれませんが、そんなのはちょっとどうかと思いますし実用的ではありません。 実用的な例をあげるとこんな感じでしょうか?

#const BLINK_INTERVAL 50
#const BLINK_INTERVAL_HALF BLINK_INTERVAL / 2

    repeat
    if( cnt \ BLINK_INTERVAL < BLINK_INTERVAL_HALF ) {
        title "ドンタコスったら"
    } else {
        title "ドンタコス"
    }
    await 10
    loop

こうすれば他の処理もちゃんとしたスピードでこなすことができるでしょう。実はこれは論理演算子を使うことでもっと小さくできて

#const BLINK_INTERVAL 16        // これは2の累乗でないとトリッキーな点滅をしてしまいます

    repeat
    if( cnt & BLINK_INTERVAL ) {
        title "ドンタコスったら"
    } else {
        title "ドンタコス"
    }
    await 16
    loop

厳密にいうと微妙に処理が変わっていて、いきなり "ドンタコス" になってしまいますが実際には大した問題にはならないでしょう。 また、ここで説明していたのはもともと「点滅」であって「文字を切り替える」ではないのですから、実際にはもう少し低コストで実装できます。

数値配列の初期化を工夫する

ショート部門とはいえ何らかの多量のデータを使用したくなることがあるでしょう。 例えば、ブロック崩しでステージを使用したい、シューティングゲームで機体のデザインを 使いたい、と数え上げたらきりがないと思います。

そんなときこのテクニックを知っているのと知っていないのとでは、相当な違いが出てきます。

まずは、自前で機体をデザインする方法で一番直感的な(多分)方法を例に出します。

// - - 定数の定義 - - // 
#const CHAR_WIDTH 8
#const CHAR_HEIGHT 8

#const DOT_SIZE 16
#define DOT_SHAPE "■"

// - - メインプログラム開始 - - //

    // キャラの座標を設定
    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    charMatrix.0.0 = 1, 1, 1, 1, 1, 1, 1, 1
    charMatrix.0.1 = 1, 0, 0, 0, 0, 0, 0, 0
    charMatrix.0.2 = 1, 0, 1, 1, 1, 1, 1, 1
    charMatrix.0.3 = 1, 0, 1, 0, 0, 0, 0, 1
    charMatrix.0.4 = 1, 0, 1, 0, 0, 1, 0, 1
    charMatrix.0.5 = 1, 0, 1, 1, 1, 1, 0, 1
    charMatrix.0.6 = 1, 0, 0, 0, 0, 0, 0, 1
    charMatrix.0.7 = 1, 1, 1, 1, 1, 1, 1, 1

    // 諸設定
    font "MS ゴシック", DOT_SIZE

    // ここで上の座標を使用して機体を描く
    repeat CHAR_WIDTH
        x = cnt
        repeat CHAR_HEIGHT
            y = cnt
            if( charMatrix.x.y ) {      // 上の charMatrix 云々の 1 に反応する
                pos x * DOT_SIZE, y * DOT_SIZE
                mes DOT_SHAPE
            }
        loop
    loop

上のスクリプトを実行すると渦巻き、もとい機体がウィンドウに絵画されます。 めんど簡単のためにこのようないい加減なものになっています。 ご容赦ください。

いえ、そんなことはどうでも良くて、問題は配列の初期化の方法です。

この方法だと、カンマも数字もそれぞれ4バイトずつ使用して、それも8x8だから 相当なコストがかかってしまいます。

カンマを大量に使用するならケーキを食べればいいじゃない、・・・じゃなくて一度適当な文字列型変数に放り込んでからgetstrでくくりだせばいいのです。 「配列の文字列の初期化を工夫する」と同じです。

その方法を応用したのが以下です。(簡単のためにこれからは座標設定に関係の無い部分は省きます。)

    // 座標を放り込んどく
    compressedCharMatrix = "1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1,0,1,0,0,0,0,1,1,0,1,0,0,1,0,1,1,0,1,1,1,1,0,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1"

    // そして展開
    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    repeat CHAR_HEIGHT
        y = cnt
        repeat CHAR_WIDTH
            x = cnt
            getstr tmp, compressedCharMatrix, idx, ','
            charMatrix.x.y = int( tmp )     // そのまま入れても型が違うって怒られるから変換してから
            idx += strsize
        loop
    loop

これで733Byte->561Byteになりました。(書いてなかったけど前の例は733Byte)

でも、まだまだ縮みます。そもそもなぜカンマを使う必要があるのでしょうか。 文字列の時はそれぞれ違う長さのデータを持ってましたが、(例えば6文字の文字列があれば、 8文字の文字列もあったりと)この例では事情が違い、全て1か0、すなわち全て1文字 になっているので実はカンマは無くても平気だったりします。

そこでカンマを取って先頭から一文字ずつ読み込んでいってはどうか、という考えで 実装したのが以下の例です。

    // 座標を放り込んどく。上とは違ってカンマが無い
    compressedCharMatrix = "1111111110000000101111111010000110100101101111011000000111111111"

    // そして展開
    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    repeat CHAR_HEIGHT
        y = cnt
        repeat CHAR_WIDTH
            x = cnt
            // strmid と getstr は文字列の返し方が違うから
            // 「一時変数に放り込んで変換」の動作をまとめられる
            charMatrix.x.y = int( strmid( compressedCharMatrix, y * CHAR_HEIGHT + x, 1 ) )  
            idx += strsize
        loop
    loop

これで561Byte->514Byteになりました。

しかしまだまだ strmid() の 引数の多さだとか、 わざわざ int() で変換しなければならないことも気にかかります。

やっぱりこれも解消できます。勘の良い方は気づいてると思いますが (勘の良い方はこんなとこ見る必要ない?) peek() を使えばいいのです。

    compressedCharMatrix = "1111111110000000101111111010000110100101101111011000000111111111"

    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    repeat CHAR_HEIGHT
        y = cnt
        repeat CHAR_WIDTH
            x = cnt
            // ここしか変わってない
            charMatrix.x.y = peek( compressedCharMatrix, y * CHAR_HEIGHT + x ) - '0'
            idx += strsize
        loop
    loop

引数が減って514Byte->506Byteになりました。って、あんま変わってないな。

これはちょっとわかりにくいでしょうけど、インターネットで「文字コード表」を探して そいつとにらめっこしながら、「peek() に文字列を与えると指定した場所にある文字コードを返す」 という事実を考えてみればわかるでしょう。多分。

大分小さくなってきましたが、まだまだです。ここで対象にしている数値は1と0、 すなわち1bitもあれば表現できてしまうのです。 それなのに一時変数に使用している変数は一文字辺り 1Byte(8bit) も使用してしまっていて非常に無駄です。

はぁ、やっとここまでこれました。ここからが数値配列変数の初期化のほんとの醍醐味です。 ここから数値配列の初期化とは少しずれて、このスクリプトに依存した説明 になってしまいますが、辛抱ください。

と、その前にちょっと準備。以下のコードを実行するとどうなるでしょうか?

    repeat
    mes 1 << cnt
    loop

これは2の累乗が出力されます。というのも、<<というのは直前の数値に2の累乗倍を かけるのと等価ですから。例えば x << y というと x * 2 ^ y と同じです。

ではこれは?

    foobar = 123

    repeat 8
    mes foobar & ( 1 << cnt )
    loop

これは実行すると、foobar の下位ビットから順に論理積がかけられます。 要はこれは二進展開を下から行ったのと似たような結果になります。

さらにやります。これは?

    foobar = "a"

    repeat 8
    mes peek( foobar, 0 ) & ( 1 << cnt )
    loop

これを実行すると、foobar の文字コードを下から2進展開 を行ったのと似たような結果になります。

これでわかったでしょうか?先ほどの、「表現しようとしているものは1bit なのに実際使っているのは8bit」という問題はこの文字コードをうまく使えば回避できそうです。

じゃあ文字コード使えばさっきのデータを8分の1に縮められるのか、と思ったあなた。 そうは問屋が卸しません。というのは適当にShift-Jisの文字コード表を探してもらえばわかります。 google 検索 "文字コード表 Shift-JIS"

文字コード表をみるとわかりますが、Shift-JISというのは一部使用できないコードがあるのです。 ですので8bit=0~255までの全てを使用できるわけでありません。(実はこの制限はある方法を 使えば回避できて、1~255までならば使えたりします。ただその方法を使用すると一気に つまらなくなりますし、コンテスト主催者もそんな方法を使って作られたプログラムを 望んでいないと思うのでここでは紹介しません。)

これを見る限り、0x00~0x1f, 0x7f は使用できません(正確に言うとテキストとして保存できない)。 また 0x80~0x9f, 0xe0~0xff の区間はマルチバイト文字のために使用されていて、安易に使うと 直後の文字コードが使えなくなる恐れがあります。

ですので普通に使うのなら 0x20~0x60 の合計 0x40 までの数値が使える区間が ちょうどいいでしょう。ですので実際に使えるのは結局6bitです。

とはいっても、上の例では6bitフルに使うとちょっとコーディングがしにくいですし、 計算量が増えて逆にバイトコードも増えてしまう恐れがあります。 ですので以下に4bitだけを使用した例をお見せします。

#const BIT_PER_CHAR 4
#const CHAR_WIDTH_PER_BIT CHAR_WIDTH / BIT_PER_CHAR

    // 圧縮したデータを放り込んで
    compressedCharMatrix = "//! -/%(%*-+!(//"

    // 展開
    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    repeat CHAR_HEIGHT
        y = cnt
        repeat CHAR_WIDTH_PER_BIT
            binIdx = cnt

            repeat BIT_PER_CHAR
                charMatrix.(binIdx*BIT_PER_CHAR+cnt).y = ( peek( compressedCharMatrix, y * CHAR_WIDTH_PER_BIT + binIdx ) - 32 ) & ( 1 << cnt )
            loop
        loop
    loop

506Byte->498Byteになりました。(実は元のデータが4分の1になって 64 - 16 = 48Byte が減るけど、 展開プログラムの部分でその 48Byte を上回ってむしろ増えるんじゃないかとよんでたんですが しっかり減っていて良かったです。たったの8Byteですが。)

compressedCharMatrix = .. で圧縮した charMatrix を文字列として代入します。 compressedCharMatrix に何を入れるかはもちろんプログラムを書いて求めます。 以下がその圧縮プログラムです。

    sdim out, CHAR_HEIGHT * CHAR_WIDTH_PER_BIT
    
    repeat CHAR_HEIGHT
        y = cnt
        repeat CHAR_WIDTH_PER_BIT
            binIdx = cnt
            data = 32

            repeat BIT_PER_CHAR
                data += charMatrix.(binIdx*BIT_PER_CHAR+cnt).y << cnt
            loop

            poke out, y * 2 + binIdx, data
        loop
    loop

    bsave "out", out, CHAR_HEIGHT * CHAR_WIDTH_PER_BIT
    mes out

変数 charMatrix にはあらかじめデータを代入しておき、このプログラムを走らせると 圧縮後の文字列を画面に表示し同じフォルダに "out" と言う名前のテキストを書き出します。

書き出すバイナリは(結局は文字列ですが)2進表記であらわすと以下のようになります。

0000000: 00101111 00101111
0000002: 00100001 00100000
0000004: 00101101 00101111
0000006: 00100101 00101000
0000008: 00100101 00101010
000000a: 00101101 00101011
000000c: 00100001 00101000
000000e: 00101111 00101111

これはぱっと見てわかりにくいですが 先頭4ビットは文字コードを使用可能なものとして使うための 下駄で、お尻の4ビットの方が実際のデータになります。そこで下駄を取って少しわかりやすくすると 以下のようになります。

0000000: 1111 1111
0000002: 0001 0000
0000004: 1101 1111
0000006: 0101 1000
0000008: 0101 1010
000000a: 1101 1011
000000c: 0001 1000
000000e: 1111 1111

これを4bitごとに左右反転させて、さらにみやすくするために空白を消してみます。

0000000: 11111111
0000002: 10000000
0000004: 10111111
0000006: 10100001
0000008: 10100101
000000a: 10111101
000000c: 10000001
000000e: 11111111

これをよおく目を凝らしてみてください。何か見えませんか?そうです渦巻きです。・・・じゃなかった機体です。 元のバイナリをこの渦巻きにするまでの操作を振り返ってみましょう。

  1. 先頭の4Bitを消した。
  2. 4Bitごとに左右反転させた。

最初の先頭4Bitを消した。というのは例の下駄をとる作業で(実際には0x20を引いています。これはこのプログラムで 一度に扱うデータが4Bit以下だからそう見えるだけです。)、Shift-JISの文字コードにそくしたバイナリから、 プログラムにそくした数値にするためのものです。

最後の4Bitごとに左右反転というのは、偶然そうした方が プログラムが小さくなるからそうしているだけで実際にそのとおりにする必要はありません(もちろんもとの データによります)。

実はこの作業、展開プログラムそのものです。 そして圧縮プログラムはこの逆の作業をやっています。

プログラムの細かい説明は・・・すみませんがやりません。ちょっと複雑になってしまい相手に理解させるのはちょっと 自信が無くなってしまいました。でも「上の操作を実現させるにはどうすればいいのだろう」と考えながら努力してみれば多分 自分なりに作れると思います。多分。

それに圧縮の方法はこのやり方が最善とは限りません。もっといいやり方があるかもしれませんし。

また、文字コードを使うと以下のようなソースも圧縮可能です。

    charMatrix.0.0 = 30, 10, 1, 8, 4, 7, 9, 2,
    charMatrix.0.1 = 48, 38, 38, 2, 3, 5, 32, 2
    charMatrix.0.2 = 30, 14, 3, 30, 20, 30, 20
    ..
    ..

これは、桁数がそれぞれ違うので getstr で区切っていくテクニックが使えるのだと気づくと思います (一応補足。これは数字が二種類というわけではないので、直前のテクニックは使用できません)。 でもこれは数値がみな 0x20~0x60+a の範囲に収まっていますので文字コードを使用して圧縮できます。 その方法はここではあえて提示せず、それはアナタへの宿題ということに ・・・なんて都合が良くきれいにまとまったところで、この長くてわかりにくい説明もこの辺で終わりたいと思います。

数値配列の初期化をもっと工夫する

上の「数値配列の初期化を工夫する」に示した圧縮方法はpeek/pokeを使うことで更に小さくすることができます。

いきなりですが、まずは展開プログラムの方をお見せします。

#const CHAR_SIZE_PER_BIT CHAR_SIZE / BIT_PER_CHAR
#const BIT_PER_CHAR 4
#const sizeof_int 4

    mes out
    dim charMatrix, CHAR_WIDTH, CHAR_HEIGHT
    repeat CHAR_SIZE_PER_BIT
        binIdx = cnt
        repeat BIT_PER_CHAR
            poke charMatrix, ( binIdx * BIT_PER_CHAR + cnt ) * sizeof_int, ( peek( compressedCharMatrix, binIdx ) - 32 ) & ( 1 << cnt )
        loop
    loop

repeat/loop の数が減ったことにお気づきでしょうか?

このようにpeek/pokeは指定のポインタに直接データを書き込むことができるため、特に二次元配列を対象にした場合には軽量化に役立つことがあります。

次に圧縮プログラムの方をお見せします。

    repeat CHAR_SIZE_PER_BIT
        binIdx = cnt
        data = 32
        repeat BIT_PER_CHAR
            data += peek( charMatrix, ( binIdx * BIT_PER_CHAR + cnt ) * sizeof_int ) << cnt
        loop
        poke out, binIdx, data
    loop

repeat/loopの数が減っています。

ただ先ほどの展開プログラムの方もそうでしたが基本的には「数値配列の初期化を工夫する」と大差はございません。 実を言うとこの圧縮プログラム「数値配列の初期化をもっと工夫する」とすり変えてもちゃんと動いちゃうくらいなんです。 本当に変わりがないです。

ちなみにこちらもやはり説明はございません。中途半端ですみませんがポインタやらの話が絡んでくるため説明がめんどくさい難しいのです。

自分なりにいろいろとpeek/pokeで遊んでみればおのずと分かると思いますのでがんばってみましょう。自分だけの力で解析することも大事ですよ。 いや本当にめんどくさいだけですけど。

ifをもっと考える(ちょっと忘れがちなこと)

例えば次の条件を同時に満たす状態を検出したいときどうしますか?

  1. フラグが立っているとき
  2. クリックされた瞬間

まず、1.のフラグですがこれは楽です。

if( flag ) {
    dosomething
}

のようにすればいいだけですから、次に2.のクリックされた瞬間ですがこちらも簡単です。

stick key
if( key & 256 ) {
    dosomething
}

では、この二つをどちらも満たす状態も簡単ですね。&&を使えばいいのです。

stick key
if( ( flag ) && ( key & 256 ) {
    dosomething
}

これでいいでしょうか?

実はこれではいけません。 HSPでは & だろうが && だろうがどちらも「ビット単位の論理積」として処理されてしまいますので、狙い通りに動いてはくれません。

実はこの問題は、ifをいれごにすれば解決できます。

stick key
if( flag ) : if( key & 256 ) {
    dosomething
}

えー。微妙ですね。 どこか無駄がありそうな感じです。

実を言いますと&&の代わりになる演算子があったりします。それを使いましょう。

stick key
if( ( flag ) * ( key & 256 ) {
    dosomething
}

はい。単なる"掛け算"です。ただこれが意外と代わりになってしまいます。 まぁ&&も所詮は論理"積"ですし意外でも無い気がしないでもないですが。

ちなみに"||"も特殊な状況を除いては"+"が代わりを勤めてくれる場合があります。 (こちらも論理"和"ですからきれいに対応してます) ここで言う特殊な状況とは (1 || -1)のように、それぞれを合計すると零になってしまうような状況です。 とはいってもショート部門ではそんな状況滅多に無いでしょうから特に気にする必要は無いと思いますけど。

白色を選択色にするときの小技

現在対象にしている色を白にしたいとき

color 0xff, 0xff, 0xff

のようにしていませんか?これは

pget 3200

のようにしても、よほど特殊な状況で無い限りは大体は同じになりますし、お得です。

最後に

どうでしたか?

この資料は大会に1度しか出ていない、ショート部門についてはまだまだ駆け出しの人間 が、作成したものなので「まだまだ甘い」と思われる方もいるかもしれません。

でもこれを読んで、アナタが表現しようとしているものへと一歩でも近づくことができていれば幸いです。

<<戻る