Smalltalk-72で遊ぶOOPの原点:組み込みコレクションにeach変数が使える「do」を新たに追加する

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


各要素へのアクセスに使える each変数

前回、Smalltalk-72 の重複を許さない使い方もできる動的配列 obset と通常の配列 vector には map があるものの、使い方が統一されていないことや、前者の map では処理中で各要素にアクセスするために内部表現である vec[i] を使うことなどに触れました。

各要素へのアクセスに用いる vec[i] については、Smalltalk-72 Instruction Manual には each も使えるという記述が見つかりますが、これは少し後のバージョンで実装された機能のようです。

この each を今の May30版でもなんとか使えるようにできないものかと考えてみました。

駄目なやり方 その1:Smalltalkエディタ(構造化エディタ edit)などを使って map にただ ☞ each ← vec[i]. を挿入する

edit obset とすると、Smalltalkエディタを開いてメソッド定義を編集できます。

Enter で ᗉmap ⇒ ()() に潜って中味を表示、さらにforアクションの最後の引数の () に Enter してから、input eval の直前に ☞each ← vec[i].(コピペ&転送用には "each _ vec[i]. )を Insert して Exit すれば完了です。

この方法は一見うまく動いているように見えますが、クラス内で宣言無しに使用している eachobset の外側(多くの場合はトップ)に漏れてしまって駄目です。

駄目なやり方 その2:obset クラスを再定義する

ALLDEFS に obsetソースコードがあるんだから、これを修正して再定義すればいいじゃない…と、特に SmalltalkRuby に慣れたかたはそう思われるかもしれません。

ところが残念ながら、今の Smalltalk と違い、Smalltalk-72 のクラスは再定義できません。一見できそうですが、それだと追従できないインスタンスが動作不全に陥り、システムで常時使われている obset などは再定義が終わるか終わらないかのうちにシステムがバグります。

to obset i input each : vec size end (
    %add?((size="end_end+1?("vec_vec[1 to "size_size+10]))
        vec[end]_:)
    %_?(0=vec[1 to end] find first :input?
        (SELF add input))
    %delete?(0="i_vec[1 to end] find first :input?(!false)
        vec[i to end]_vec[i+1 to end+1]. "end_end - 1)
    %unadd?("input_vec[end]. vec[end]_nil.
        "end_end - 1. !input)
    %vec?(!vec[1 to end])
    %map?(:input. for i _ end to 1 by -1 ("each _ vec[i]. input eval))
    %print?(SELF map "(vec[i] print. sp))
    %is?(ISIT eval)
    isnew?("end_0. "vec_vector "size_4)
    )

デバッガーの無限起動は escキーで止められますが、それ以降、正常な動作は望めないので、Show Nova ボタンを押して画面を切り換えてから、Restart ボタンを押し、Show Smalltalk でまっさらな状態に復帰してください。

Smalltalk の動的性の象徴ともいえる、こういったクラスの根幹を変える変更にも何食わぬ顔で対応できるようになるのは、ポインタのすげ替えを可能にする become: により作り直したインスタンスを置き換える力業が導入された Smalltalk-76 以降なのです。

駄目な方法 その3:Smalltalkエディタの title オプションを使う

Smlltalk-72 Instruction Manual を読み進めると、Smalltalkエディタは起動時に title オプションを付けることで、メソッド本体だけでなくクラス名や各種変数の宣言を(通常のメソッド編集と同様に)動的に変更できそうだと期待させられます。

ですが、これも駄目です。やってみると分かりますが、クラスの再定義と結果は一緒で、each: の前に Insert してから Exit すると同時に画面がバグります。復帰方法はすでに書いた通りです。

edit のソースを読むと分かりますが、メソッドは編集後のベクターをただ eval しているだけなので新たにクラスを定義し直すのとまったく変わりませんね。

少しマシな方法: クラス変数に each を追加して map☞each ← vec[i]. を追加する

前述のとおり、Smalltalk-72 のクラスは、テンポラリ変数やインスタンス変数を追加することはできませんが、クラス変数は比較的自由に追加することが可能です。クラス変数を追加したり、既存のそれらの内容を変更するには PUT アクションを使用します。

たとえば each というクラス変数を初期値 nil で追加するには PUT obset ☞each nil(コピペ&転送用には PUT obset "each nil )を評価します。

これなら each が漏れ出すこともありません。

ところで map のブロック代わりの引数は vectorリテラル ☞( ... ) で渡すのですが、for アクション for i to 5 do (i print. cr). のように括弧のみで書ける方が見た目がすっきりしますよね。forアクションでは最後の引数をリファレンスとしてフェッチ :#exp することでこれを実現しています。

そんなわけで、ここでは次に紹介する方法が使えない vector に、eachが使えて、引数にクオートアクション が不要な do を定義します。(図には obset に適用した場合の様子も示していますが、ここでは行わないでください。)

PUT vector "each nil
addto vector "(%do ? (:#y. for x to SELF length ("each _ SELF[x]. y eval)))
"(1 2 3) do (each print. cr).

少しひねりを入れた方法:クラス変数 eacheach アクションを代入する

この方法は obset にしか使えないのですが、新しく定義する do はもちろん、コード変更無しで既存の map にも each が利用可能になるワザです。

先の vector の例と同様に、新しく定義する do では、obset の逆順になる謎仕様も昇順に変更しています。

to t each (ev)
t
to each (!vec[i])
PUT obset "each #each
done

addto obset "(%do ? (:#input. for i to end (input eval))

"set _ obset. set _ 1. set _ 2. set _ 2. set _ 3.
set do (each print. cr).
set map "(each print. cr).

おおよそ次のような手順のことをやっています。

まず、アクション(インスタンスを生成しないクラス)t を定義して、そこにこれから定義するアクション each と同名のテンポラリ変数を宣言しておきます。アクションのメソッド本体は ev つまり REPLです。なお t は特にクラスなどのテンポラリな名前に Smalltalk-72で慣習的に使われます。

次に、アクションt をコールして t のコンテキストで REPLを動かします。そこで今回必要になるアクション each を定義します。通常、アクションto へのメッセージ送信にによるアクションを含むクラスの定義は、クラス名がトップ(実体は USERアクションのクラス変数)に追加されますが、t にはテンポラリ変数 each が宣言されているので、ここで漏れ出しが食い止められます。

そして PUT アクションで obset のクラス変数 each を追加して、そこに今定義したアクション each を代入します。

最後に done で REPLを抜けて元の REPLに戻っています。あとは、addto アクションで doメソッドセクションを obset のメソッドに追加して終わりです。

いろいろ便利なので、以降はこの doeach変数を積極的に使ってゆこうと思います。

とりあえず、前回の spacewar はこのように書き換えられます。

to spacewar x y z : : objects (
    (null objects ? ("objects _ obset))
    %schedule ? (objects _ :#)
    %delete ? (%all ? ("objects _ nil) objects delete :#)
    %run ? (repeat (objects do ("x _ each. x)))
)

宇宙船の残像を消す へ続く)

Smalltalk-72で遊ぶOOPの原点:簡易スケジューラーを作る

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


obset を活用して簡易スケジューラー spacewar を定義

前回の動作チェックで使った repeat の代わりに、新たに spacewar アクションを定義し、これにその役割を担わせることにしましょう。

こんな仕組みと動きを想定します。

  1. objects というセットもどき(obset)をクラス変数として持ち、
  2. spacewar schedule obj で宇宙船や、将来は魚雷などのオブジェクトを objects に追加
  3. spacewar delete obj で指定したオブジェクト、spacewar delete all で全てのオブジェクトのスケジュールを放棄
  4. spacewar runobjects 内のオブジェクトに順に制御を渡す

クラス変数 objects に代入される obset は、ALLDEFS(ブートストラップ)で定義されている、要素をユニークなもののみに保つセットの性質も有するシンプルな動的配列オブジェクトです。

☞set ← obset.インスタンスを生成し、set ← elem とすることで、elem がはじめてなら追加、すでに追加済みなら無視します。

obset は、この elem のユニーク性を維持する ← elem だけでなく、無条件で追加する add elem もあるので、こちらを使えば要素の重複を許す通常の動的配列としても使えます。

ただし、動的“配列”とはいうものの、インデックスでのアクセスなど配列の基本的な機能はいっさい用意されていないので、そういう用途には vec で内部表現のベクター vectorインスタンスを引っ張り出して使うという割り切った仕様になっています。

前述のとおり、ALLDEFSウインドウ、もしくは show obset でその定義を見ることができます。

後の Smalltalk-80 で導入される do:Rubyeach に相当する map も素朴な実装ながら備えられています。

www.evernote.com

ところで、組み込みの配列 vector にも map はあるにはあるのですが、この obsetmap とは違って、各要素に map の引数となる配列内に収めたシンボルをメッセージとして送るだけという別の仕様になっています。こちらは、Smalltalkdo:Rubyeach でブロックの代わりにシンボルを与えたときの使い方に似ています。同じ map でもぶれがある雑さがいいですね^^;

spacewarアクションはこんなふうに定義してみました。

to spacewar x y z : : objects (
    (null objects ? ("objects _ obset))
    %schedule ? (objects _ :#)
    %delete ? (%all ? ("objects _ nil) objects delete :#)
    %run ? (repeat (objects map "("x _ vec[i]. x)))
)

disp clear
"s1 _ spaceship 'Jimmy' 5 30 false.
"s2 _ spaceship 'Beth' `5 `20 false.
spacewar schedule s1. spacewar schedule s2.
spacewar run

前回同様に2艇の宇宙船 s1 s2 を定義して schedule 後、run してみましょう。

escキーで停止できます。

組み込みコレクションにeach変数が使える「do」を新たに追加する へ続く )

Smalltalk-72で遊ぶOOPの原点:宇宙船「spaceship」の仮実装と複数インスタンスの動作確認

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


描いて移動するだけの spaceship

複数の宇宙船のキーボードからの同時操作を早く試さないといけないので、まず移動 moveship と描画 display ship のみに限定した spaceshipSmalltalk-72で実装します。

to clock (!mem 280)

to spaceship : pilot thrust steer trigger numtorps location speed direction time (
    isnew ? (:pilot. :#thrust. :#steer :#trigger. 
        "numtorps _ "speed _ "direction _ 0.
        "location _ point (rand between 50 462) (rand between 50 462).
        "time _ clock)
    0 < clock - time + MOVELAG ? (
        "time _ clock + MOVELAG.
        moveship.
        display ship)
    )

"SSIZE _ 6. "MOVELAG _ 0. "SPSCALE _ 1.0. "DIRSCALE _ 1.0. "LSCALE _ 1.0.

システムクロックの取得については mem 280 のままでもよさそうですが、Smalltalk-71の元のコードに見た目を似せるため clock アクションを作りました。(不明点が多かったり、いろいろ破綻してきているので、もはや「元のコードに似せる」努力自体、無駄なあがきのようにも思えますが…^^;)

spaceship はこれまでのプロシージャと違い、クラスとして機能する(インスタンス生成能を持つ)と仮定して、Smalltalk-72版ではインスタンスを生成する isnew とその非偽時、つまり、インスタンス生成直後のコンテキストである場合に実行される初期化処理を加えています。

仮引数の pilotthruststeertrigger は送られてきたメッセージから引数としての値をフェッチ : しています。後三者については、逐次評価して値が変わるオブジェクトであることを想定して(将来的にはジョイスティックをエミュレートするオブジェクトを渡す予定)、通常の値のフェッチ : ではなく参照をフェッチ :# しています。

その他のインスタンス変数については、適当な値で初期化しました。

非同期処理を想定したと思われる pause ... のところは悩ましいところですが、指定した間隔を置いて処理をするように最初に移動しました。もっとも、このエミュレーターは大変遅いので、画面更新や位置情報の更新のためのラグを考慮する必要はほとんど無いと思われます。

なおここで使用している MOVELAG を含めたグローバル変数については、すべて大文字に変更しました。加えて、Smalltalk-71版に登場する :size については何かと問題が発生したため SIZE からさらに SSIZE に名前を変えています。なお、Smalltalk-71 でプロシージャの仮引数として :size を使っているものは変更無しです。

これをうけて moveshipdisplay については次のようにコードが変更になります。どうぞあしからず。

to moveship (
    "speed _ speed + SPSCALE * thrust.
    "direction _ (direction + DIRSCALE * steer) mod 360.
    location x _ (location x + (cos direction) * LSCALE * speed) mod 512.
    location y _ (location y + (sin direction) * LSCALE * speed) mod 512.
)

to display (
    @ penup goto location x location y up turn direction + 90 pendn.
    (:#) SSIZE.
    (0 < thrust ? (flame SSIZE)
    0 > thrust ? (retro flame SSIZE))
)

グローバル変数の初期値はそれぞれ、上図およびコピペ用のコードに示したとおりです。

spaceship について、あとは、moveshipdisplay ship だけをコールするようにコードしました。

では、さっそく動かしてみましょう。とりあえず、thruststeertrigger530false に設定しています。

disp clear
"s1 _ spaceship 'Jimmy' 5 30 false.
repeat (s1)

良い感じですね。宇宙船を増やしてみましょう。

"s2 _ spaceship 'Beth' `5 `20 false.
repeat (s1. s2)

あらゆる年齢の「子供たち」のための パーソナルコンピュータ

esc キーで停止します。

簡易スケジューラーを作る へ続く)

Smalltalk-72で遊ぶOOPの原点:疑似乱数発生器を実装する

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


16ビット用Xorshfit疑似乱数発生器

さて、そろそろ Smalltalk-72 で spaceship を実装してキー操作や(疑似)並列化などいろいろ試したいところですが、そのインスタンス化のときの locationdirection の初期化に際して元の Smalltalk-71版には無い適当な初期値を与える必要があります。ランダムにしておくのが順当そうですが、Smalltalk-72 にはもちろん(?) 乱数発生器は無いのでそのためには自分で用意しなければなりません。

Smalltalk-72 Instruction Manual に簡単な実装例があるのですが、少しひねりを入れてこれを Xorshift版に書き換えましょう。

Xorshiftは、こちらの16ビット用のコードを参考にさせていただきました。

www.retroprogramming.com

以下で Smalltalk-72 May30版で書いたコードを順に見てみましょう。(コピペ用のテキストは文字化けぎみなので、図の方をご参照ください。)

to rand low high : : n (
   (%seed ? (:n))
   (null n ? ("n _ 12345))
   "n _ n &- n &/ 7.
   "n _ n &- n &/ `9.
   "n _ n &- n &/ 8.
   %between ? (:low. :high. !low + n mod high + 1 - low)
   !(32768.0 + n) / 65535.0)

rand

rand between 1 10

randインスタンス生成能を持たないクラス(=アクション)で、テンポラリ変数 low および high、クラス変数 n が宣言されています(to rand low high : : n (... )。

なお、上で引用した Smalltalk-72 Instruction Manual のバージョンでは、変数種類の区切りは | ですが、エミュレーターの May30版の時点ではまだ : が使われています。( to クラス名 テンポラリ変数宣言 : インスタンス変数宣言 : クラス変数宣言 (メソッド記述)

sumim.hatenablog.com

この rand アクションは、seed というメッセージを受け取ると( ᗉseed ⇒ (... )、続くメッセージを式として評価して値を取り込み、クラス変数 n に代入します(:n)。ちなみに :n. は、☞n ← :.(つまり、アクション への n の送信の返値への ← : の送信)を、: アクション : へのメッセージ n の送信として表現した、同じ処理(取り込んだ値の変数への代入)の別の書き方です。

ここまできてまだ nnil なら( null n ⇒ (...12345 を代入します(`☞n ← 12345)。

Smalltalk-72 で ビットXOR は + +、ビットシフトは + / の記号で表され、それぞれ & + もしくは & / の順にタイプすることで入力できます。これは というグリフの文字幅が 1 と実際の文字の幅よりずっと狭く設定されており、続けて何か文字(この場合 + もしくは / )が表示されるとそれが重なって見えるカラクリで実現されています。

クラス number の主要部分はプリミティブ( CODE 4 )なので show number では肝心な部分の定義を見ることはできませんが、イメージとしては ALLDEFS のコメントに書かれているこんな感じ(次図のハイライト部分)で実装されていると思えばよいでしょう。

余談ですが、Smalltalk-80以降と違い、Smalltalk-76 までは連続した記号をまとめてひとつのメッセージシンボル(二項セレクター)としては扱えませんでした。Smalltalk-76 には特にワークアアラウンドは用意されませんでしたが、Smalltalk-72 では連なる記号を1文字ずつ確認( )しつつ条件分岐(⇒(...)する処理を重ねれば、これらビット演算のように、なんとか任意の記号の組み合わせによる二項演算表現も可能だったようです。

閑話休題

さらに rand アクションは between というメッセージを受け取ると( ᗉbetween ⇒ (... )、続く2つの数値(それぞれが式で与えられている場合は、それを評価してから)取り込み、それぞれ low および high に代入します( :low. :high. )。そして、まず n について highlow の差に 1 を足した数の剰余を求めてから( n mod high + 1 - lowlow にそれを加えることで、low から high までの値に正規化して返します( ⇑low + ... )。

Smalltalk-72 での演算は、オペレータに相当するメッセージシンボルをルック()したあと第二オペランドを、続くメッセージを式として評価してフェッチ(:)することで得る都合上、そのほとんどが右結合になることに注意してください。これは、効率化のため動的な関数呼び出しにメッセージングの“フリ”をさせている Smalltalk-76 以降(演算は左結合)と Smalltalk-72 との大きな違いです。

最後に、sendbetween などのメッセージが送られることなく、たんに rand とした場合は 0 以上 1 未満の値を返します( ⇑(32768.0 + n) / 65535.0)。

宇宙船「spaceship」の仮実装と複数インスタンスの動作確認 へ続く)

Smalltalk-72で遊ぶOOPの原点:宇宙船をキーで制御する方法を考える

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


キーの押下を検知する kbck の重要性

moveship のときにも書きましたが、spaceship の仮引数の宣言と start でのその呼出しを見ると、この Smalltalk-71 のコードではジョイスティックの 横(y軸)方向の動きを :steer として、縦(x軸)方向の動きを :thrust として(そして、ジョイスティックに付属するボタンの押下を :trigger として)得ていることがわかります。

Smalltalk-72への移植に際し、宇宙船一隻だけならマウスの動き and/or ポインタの位置(トリガーについてはボタンの押下)でこれをエミュレートするという方法もありそうですが、少なくとも二隻による対戦も可能にしたいので、その方法は用いず、特定のキーの連打で対応するスティックの傾きを増減を表現するような制御方法を考えます。

Smalltalk-72 では押下されたキーはキューイングされ、それらは kbd で一つずつ取り出せます。もしキューイングされたキーが無い状態で kbd を呼ぶと、キーが押されるまで待ち状態になります。

処理待ちキーがキューに有るかどうかは kbck で調べられるので、kbd を呼ぶ前にそのチェックをすれば、キー待ちで処理がロックされるを防ぐことができます。

たとえばこんな感じ…

"prev _ mem 280.
repeat (
    repeat ((mem 280) > prev + 166 ? ("prev _ mem 280. done)).
    "key _ 0.
    prev print. sp. repeat (kbck ? ("key _ kbd print. sp) done)
    32 = key ? (done) cr.)
☞prev ← mem 280.
'prev にシステムクロック値を代入'
repeat (
'ループ処理①'
    repeat ((mem 280) > prev + 166 ⇒ (☞prev ← mem 280. done)).
    'ループ処理②'
    'システムクロック値が prev + 166 より大きければ prev を更新してループ処理②を抜ける'
    ☞key ← 0.
    'key に 0 を代入'
    prev print. sp. repeat (kbck ⇒ (☞key ← kbd print. sp) done)
    'prev出力後、スペースを出力しループ処理③'
    'キューにキーがあれば kbd でひとつ取り出し key に代入して出力、無ければループ③を抜ける'
    32 = key ⇒ (done) cr.)
    'key が 32(スペースキー)ならループ処理①を抜ける'

もし kbck によるチェックをせずにダイレクトに kbd からキーをポップするだけでこの処理を書こうとすると、この例のように「システムクロック値(prev)の等間隔での出力」のような他の並行する処理を滞りなく行うことができなくなってしまいます。

これを踏まえて、宇宙船(spaceshipインスタンス)を複数制御するためのスキームとしては、おおよそ次のようなステップが考えられそうです。

  1. ある宇宙船の thruststeer それぞれを増減させるキーのペア(trigger はそのままそれらとは別のキーと )の対応表をどこかに保持しておき
  2. キー検出を担当するアクションが自分のターンでその表を参照しながら
  3. 押下されたキーに対応する宇宙船の thruststeer(そしてtrigger)の状態を変化させる

つまり、宇宙船や魚雷のインスタンスたちと同列に、このキー検出を担当するアクションにも(疑似)並列的に制御を渡してやればよいことがわかります。

同時にようやく、Smalltalk-71版の spaceshippause until clock = :time + :movelag や、displaypause until clock = :time + :framelagSmalltalk-72 でどのように処理すればよいかがなんとなく見えてきた気がします^^;

疑似乱数発生器を実装する に続く)

Smalltalk-72で遊ぶOOPの原点: Altoのシステムクロック取得

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


エミュレーターにちょっとだけ手を加えて mem 280 の数値をそれっぽく動かす

そろそろ並列性をどう実現するか考えなければならなくなってきたので、同期をとるのに必要っぽそうな Altoのシステムクロックの取得について。

ALLDEFS の説明を読むと Smalltalk-72 では mem 280 で Altoのシステムクロック値を取得できることになっています。

しかし、オリジナルのダン・インガルス版では、恐らくメモリダンプされた時点のシステムクロック値を返すだけでこれは機能していないようです。

Altoのハードウエアマニュアルによれば、同マシンのシステムクロックは、38.08マイクロ秒毎にインクリメントされる 26 bit 値で表現される仕様なのだそうです。

だいたい 40分くらいで一周する(元の値に戻る)この 26 bitのうち上位の 16 bit が 280番地に保持されます(念のため、8進数の430は 10進数の280)。

なので、この 280番地を適当なタイミングでインクリメントしてやれば、それっぽい動作をさせることが可能だとわかります。

Altoのエミュレーターの画面を Ctrl+クリック でハロー(選択状態にあるウィジェットの周囲に表示されるいろいろなメタ機能を持つ○型ボタン群)を呼出し、右側中央にある Debugハローをクリックすると Script Editor を開いてエミュレーターのコードを読んだり編集することができます。

左手のメソッドリストから step を呼び出すと 280番地を書き換えるのによさそうなループが見つかります。

これに this.mem[280]++; を追加して保存したワールドがこの一連の記事で使用している tweak3 です。

変更後、mem 280 の値が(正確ではないですが)変化するようになります。

手元の環境では約30ミリ秒でインクリメントされ、約32分、つまり Altoの実機より少し早めに一周するようです。

宇宙船をキーで制御する方法を考えるへ続く)

Smalltalk-72で遊ぶOOPの原点:「retro」と「display」の仮実装

アラン・ケイの“メッセージングによるプログラミング”という着想に基づき(非同期処理などいろいろ足りていないながらも──)比較的忠実に実装された1970年代の非常に古いSmalltalk-72に実際に触れてみるシリーズ 第2弾です(なお最新のSmalltalkについては Pharo などでお楽しみください!)。

今回は謎言語「Smalltalk-71」で書かれたスペースウォー・ゲームSmalltalk-72に移植して動かすことを目指します。前回(2019年)を含む他の記事はこちらから→Smalltalk-72で遊ぶOOPの原点 | Advent Calendar 2023 - Qiita


内容が不明の retro は何か?を意識しつつdisplay を眺める

moveship がうまく機能しているのかを宇宙船を表示させて確認したいので、プロシージャ display を実装します。内容を見てみましょう。

to display ":obj
  penup, moveto :location, turn :direction
  create :obj :size
  if :thrust > 0 then create flame :size
  if :thrust < 0 then create retro flame :size
  pause until clock = :time + :framelag
end to

":obj により、引数を通常の方法とは違う方法で受け取っているようです。

コールする側の shpaceship では display ship のように記述していますが、このとき ship プロシージャをコールしてしまってはおかしなことになるので、 ship プロシージャそのものを引数として渡していると考えるのが自然です。

display プロシージャ本体では、まず、penup, moveto :location, turn direction でタートルを :location の場所に移動し、:direction の向きに変更しています。

そして続く create :obj :size で、指定した大きさの宇宙船を描いている(ship :size)みたいなのですが、ここでの create の役割が謎ですね。

spaceship の魚雷(torpedo)生成はこれがクラスなのでインスタンス生成だろうと想像していたのですが、:obj として渡された ship はオブジェクトではなくプロシージャなのでまったく当てが外れてしまったみたいです^^;

次で、:thrust が正なら通常の場所に、負ならおそらくは逆噴射の場所に flame:size で描きます。ここにも create が登場します。また、retroflame を修飾する副詞のような使われ方に見えます。

最後の pause until clock = :time + :framelag は、spaceshippause until clock = :time + :movelag と同様に、並列処理を意識した記述と思われますが、並列性の実現については後で考えることにして今はとりあえず無視しましょう。

retrodisplay を実装して moveship の動きを再び確認

create を気にしなければ、retro にはタートルの向きを逆にする操作を書くだけで、本来、宇宙船の尾部に描くはずのスラスタの炎(flame)を宇宙船の頭に描いてやることができます。

とりあえず今は、そのような解釈で display とともに Smalltalk-72 で書いてみることにします。

to retro (@ turn 180)

to display obj (
    @ penup goto location x location y up turn direction + 90 pendn.
    :#obj size.
    (0 < thrust ? (flame size)
    0 > thrust ? (retro flame size))
)

Smalltalk-72 May30版のタートルには、その向きをダイレクトに指定する方法が無いので、up により、あらかじめ決められた向き(270度)に変更してから、宇宙船の向きdirection にさらに 90(=360-270)度を足す角度を回転 turn させることで Smalltalk-71版と同じ効果を出しています。

Smalltalk-71 の display で引数として(Smalltalk-72 ではメッセージとして)特殊な受け取り方をしている obj については、create の役割が分からない状況では判断が難しいところですが、要は obj として受け取ったこの場合の shipobj size のように記述することで ship size と同じようにコールできればよい…と割り切れば、Smalltalk-72 ではメッセージとして送られてきた ship を参照としてフェッチして obj に代入(:#obj)することで目的は達成できます。

余談ですが、obj はこの一度しか使わないので、Smalltalk-72 の場合、objというテンポラリ変数の宣言を含め obj を取り除いてしまうことも可能です(参照フェッチ :# を括る括弧は新たに必要になりますが…)。

to display (
    @ penup goto location x location y up turn direction + 90 pendn.
    (:#) size.
    (0 < thrust ? (flame size)
    0 > thrust ? (retro flame size))
)

最後の thrust の正負で分岐する処理は、今のところdisplayアクションのメソッドの最後の処理なので、全体を括る括弧はあっても無くても大丈夫ですが、この後に(thrust の正負にかかわらず)実行しなければならない処理があるときは括弧は必須です。念のため。

前回の moveship の動作確認のためのコードに組み込んで実行してみましょう。

disp clear
"location _ point 200 200. "speed _ 10. "spscale _ 1.0. "thrust _ 10. "direction _ 270. "dirscale _ 0.5. "steer _ 90. "lscale _ 1.0. "size _ 6.
do 10 (moveship. display ship).

逆噴射版も。

disp clear
"location _ point 200 200. "speed _ 10. "spscale _ 1.0. "thrust _ `10. "direction _ 270. "dirscale _ 0.5. "steer _ 90. "lscale _ 1.0. "size _ 6.
do 10 (moveship. display ship).

描いた宇宙船はいつどうやって消しているんだよ…という新たな謎が生じますが、とりあえずはこれでよしとしましょう。

Altoのシステムクロック取得 へ続く)