Smalltalk-72で遊ぶOOPの原点:改めて「moveship」の実装

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

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


悩ましい宇宙船制御の方法

moveshipOCR版で再掲します。

to moveship
  make :speed be :speed + (:spscale * :thrust)
  make :direction be :direction + (:dirscale * :steer) rem 360
  make :location:x be :location:x + (:lscale * :speed * cos :direction) rem 1024
  make :location:y be :location:y + (:lscale * :speed * sin :direction) rem 1024
end to

:spscale:dirscale:lscale はそれぞれ、速度、方向、座標の補正用パラメータで、グローバル変数として与えられているようです。

:thrust:steer は、start を見ると、spaceshipインスタンス化の際に、第二、第三引数として渡される関数で、その時点のジョイスティックの状態を返しているように読めます。恐らくは、スティックの倒し具合と倒した向きが返ってくるのでしょう。

Smalltalk-72 への移植では、このジョイスティックの代わりをどのように実現するかも悩ましいところです。マウスを使ってジョイスティックをシミュレートする方法もありますが、対戦時には使えないので、やはり、加減速や転回のためのキーをそれぞれのプレイヤーにアサインしておき、それを連打することで、 :thrust および :steer が返す値を変える…というのがよさそうです。

ただここにもひとつ問題があって、キー押下のイベントについては、Alto および Smalltalk-72 のスペックどおりなら mem 65052 で取れそうに思うのですが(ALLDEFS 参照)、残念ながら Lively-Web 版エミュレーターではこれが機能していないようで、マウスクリックのイベント取得を可能にしたときのように JavaScript レベルで VM にパッチを当てるか、あるいは何らかのキーを押さないと処理がそこで止まってしまうという制約付きながら kbd を使うか等のワークアラウンドが必要になると思います。

[追記:その後、キー入力の有無を返す kbck を見つけた^^; ので、これと入力されたキーを順に返す kbd を組み合わせることで問題はなさそうだと分かりました。]

:location:(:x :y) を表現するための point の定義

Smalltalk-72 には Smalltalk-76 以降にある Point というクラスが無いので、今回の :location:(:x :y) を扱うために定義します。

to point : x y (
    %x ? (%_ ? (!:x) !x)
    %y ? (%_ ? (!:y) !y)
    %print ? (disp _ '(point '. x print. disp _ 32. y print. disp _ ')')
    isnew ? (:x. :y)
)

"pt _ point 3 4. pt
pt x
pt y
pt x _ pt x + 10. pt
pt y _ pt y + 20. pt

これは Smalltalk-72 のクラスの定義の典型例にもなっています。

まず、インスタンス生成をしないアクションでは、toに続く名前の直後にテンポラリ変数を宣言していましたが、インスタンスを生成する場合は : で区切ってからインスタンス変数を(必要なら)宣言します。フェッチアクション : と紛らわしいですが、この区切りの : はただの記号です。この分かりづらさを解消するためか、Smalltalk-72 の後のバージョンでは、| に変更されます。今回は使用しませんが、さらに : で区切ってクラス変数(インスタンスとクラスで共有)を宣言することもできます。

次に、クラスにインスタンス生成能を持たせるためにはメソッド中に必ず isnew の評価が必要です。isnew はクラスのコンテキストではインスタンス(非偽値)を返してコンテキストをそれにスイッチし、インスタンスのコンテキストでは false を返します。したがって、そこで条件分岐( テスト ⇒ ( 非偽値時処理 ) 偽時処理 )を書けば、非偽値時処理をコンストラクタ代わりに使えるというカラクリです。

この point の例は、メッセージとして送られてきた2つの値をフェッチ(:)して、それぞれ x および yアサイン(:x :y)しています。

ᗉx ⇒ ...のメソッドセクションは、アクセッサーに相当します。メッセージとして x を受け取ったとき、続くメッセージが代入を意図する ならば続くメッセージを(評価して)フェッチ : し、x に代入してから改めてその値を返します(⇑:x)。続くメッセージが でなかったときは、そのまま x を返します(⇑x)。y についても同様です。

ᗉprint ⇒ ... のセクションでは、pointインスタンスを表示するための処理を記述しています。コピペ文化の無い Smalltalk-72 の場合あまり意味はないのですが、Smalltalk-80以降を真似て、評価すると同じオブジェクトを生成できる表示にしました。

あとはクラスを問われたときの isis? への応答の実装なども欲しいところですが、今回の Spacewar の移植にはあまり出番が無さそうなのでやめておきます。

moveship の実装

準備が整ったので、本題の moveshipSmalltalk-72 で書いてみましょう。

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.
)

"location _ point 200 200. "speed _ 10. "spscale _ 1.0. "thrust _ 10. "direction _ 270. "dirscale _ 0.5. "steer _ 90. "lscale _ 1.0.
do 10 (moveship. {speed direction location} print. cr).

図では、必要なパラメータをあらかじめセットしておき、moveship を 10回ほどコールしてみています。

数値だけだとわかりにくいですが、どうやらうまく動いているようです。

「retro」と「display」の仮実装 へ続く)

Smalltalk-72で遊ぶOOPの原点:「sin」「cos」の実装

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

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


宇宙船の位置を更新する moveship で使われている sincos

spaceship クラスで、宇宙船の位置を更新するプロシージャがこちらの moveship です。

宇宙船の速度と向きから新しい位置を算出するのに三角関数が使われていますが、abssqrt すら無い Smalltalk-72 に三角関数があるはずもないので例によって雑に(^^;)実装しておきます。

余談ですが、正弦関数 sinsine) にからめては、1975年の「Personal Computing(パーソナル・コンピューティング)」にアラン・ケイによるこんな記述がありますのでご紹介します。

外から見ると、ブラックボックスが返事を返すために使っている方法(メソッド)については、私たちはその詳細を知りえない。たとえば私たちが次のようにリクエストしたとき

  sine 30

その返答がルックアップテーブルから得られた値なのか、チェビシェフ近似で計算されたものなのか、無限級数の和を使って計算されたものなのか、あるいはこれらの組み合わせなのかはわからない。そして実際、私たちは、期待される返答が一貫して、素早く、私たちが行っているかもしれない他のことを妨げることなく戻ってくる限り、まったく気にする必要がない。

欲求(意味的な概念)と方法(実用的な概念)を分離することが、Smalltalkの中心にある原則である。

ここでは、無限(?)級数の和(テーラー展開)を使うことにします。

まず整数で結果を返す階乗 nfact が必要です。実装は簡単ですが、16ビットの符号付き整数はすぐにオーバフローしてしまうので 7 が限度な点に要注意です。(もとより Smalltalk-72 には、Smalltalk-80 で実装された多倍長整数 LargeInteger も、小整数 SmallInteger からそれへの自動コーション機構もまだありません。)

to nfact acc n ("acc _ 1. for n to (:) do ("acc _ acc * n) !acc)

そして sincos はこんな感じにしました。

"PI _  3.14159265

to sin acc x n m (
    "x _ (:) mod 360.
    (180 < x ? ("x _ x - 360))
    (90 < x ? ("x _ 180 - x)
    `90 > x ? ("x _ `180 - x))
    "x _ (PI / 180) * x.
    "acc _ 0.0.
       for n _ 0 to 3 do (
        "m _ 1 + 2 * n.
        "acc _ acc + ((`1.0 ipow n) * (x ipow m) / nfact m))
    !acc)

to cos (!sin 90 + :)

この算出方法は、角度の絶対値が大きくなると誤差が大きくなるので、与えられた角度は -9090 に正規化してから計算しています。なお πグローバル変数 PI で与え、cos は位相をずらすことで定義しています。

ipowfloat に組み込みの整数乗の演算です。show float で表示される floatクラスの記述の中の ᗉipow ⇒セクションで定義を参照できます。

え? ブラックボックスなのにこんな簡単に定義が読めたり書き換えられても良いのか?

いえいえ。そうすることに合理的理由があるのなら、オブジェクトは内部をさらしたり、中味の作り直しを許すことも(まあ好ましくはないわけですが)アラン・ケイの「オブジェクト指向」の枠組みから必ずしも外れてはいないのです。

改めて「moveship」の実装へ続く)

Smalltalk-72で遊ぶOOPの原点:「spaceship」を解析する

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

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


どうやら宇宙船クラスらしい spaceship をざっと読む

爆発を描く flash 、逆噴射の retro 、魚雷を描く torp は省略されているので後回しにします。

torpedo との類似性などを参考にしつつインデントを整えるとこんな感じでしょうか。

to spaceship :pilot :thrust :steer :trigger
  use :numtorps :location:(:x :y) :speed :direction
  repeat
    moveship
    if :trigger and :numtorps < 3
      then create torpedo :speed :direction :location
    ?crash :self
    display ship
    pause until clock = :time + :movelag
end to

Smalltalk-71 のクラスも、Smalltalk-72 のクラス同様に、クロージャのような性質を強く持ち合わせているようです。

この定義と start の記述から、spaceshipインスタンス化時=呼び出し時に :pilot:thrust:steer:trigger という4つの引数をとり、それぞれ、パイロット名と宇宙船制御のための入力デバイスアサインしているように読めます。

続くuse文は、前述の呼び出し時の引数以外のインスタンス変数を宣言しているように読めます。:location:x:yというフィールドを持つ、アドホックに宣言できるレコードのようなものでしょうか。

次のような処理が repeat により繰り返されます。

  1. 宇宙船を移動 moveship
  2. もし発射ボタンが押されていて :trigger 、まだ魚雷が残っていたら発射 create torpedo
  3. 衝突判定 ?crash
  4. 宇宙船の描画 shilp
  5. 単位時間経過を待つ pause

ループは回り続けるようなので、並列性を意識した仕様のようによみとれます。Smalltalk-72 にはそうした機能はないので、なにか方法を考えないといけませんね…

「sin」「cos」の実装へ続く)

Smalltalk-72で遊ぶOOPの原点:「sqrt」を実装する(コードと解説編)

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

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


sqrt アクションを定義する

これで準備が整いましたので sqrtニュートン法でサクッと実装してしまいましょう。

to sqrt x y z (
    0.0 > :x ? (error)
    0.0 = x ? (!0)
    "y _ 1.0 * x.
    "z _ 1.0e`8 * x.
    repeat (z > y - "y _ y - ((y * y) - x) / y * 2 ? (done)).
    x is float ? (!y)
    z > abs y - "x _ 1 * y + z ? (!x) !y)

まず、テンポラリ変数として x y z を冒頭で宣言します。

メッセージとして送られた数値(入力値)を x にフェッチしつつ負ならばエラー(0.0 > :x ⇒ (error))、ゼロなら 0 を返します(0.0 = x ⇒ (⇑0))。

次に入力値 x1.0 にかけることで(たとえ整数であった場合でも) float に変換し、y に代入します。Smalltalk-72 では代入も ☞y の返値であるアトム(シンボルの実体)へのメッセージ ← 1.0 * x の送信です。

z には許容する誤差として、さしあたり入力値の 108 分の1くらいにしました。

あとは yy - (y^2 - x) / 2y の差の絶対値(前回定義した absアクションを使用)が z より小さくなるまで繰り返せば平方根が求まります(repeat (z > y - ☞y ← y - ((y * y) - x) / y * 2 ⇒ (done)).)。

注意する点は、まず、演算が右結合なので必要なら括弧を適切に使用することです。乗除優先でないことを意識して括弧を用いる点は Smalltalk-80 でも同じですが、右結合か左結合かの違いがあるので、Smalltalkをよく知ってる場合でも要注意です^^;

そして、浮動小数点数と整数との演算で浮動小数点数を返したいときは、整数がレシーバーにならないようにする(交換則が成り立つならメッセージ側に移動、もしくは、今回は該当しませんがそうできない場合は -アクションのように、あらかじめ整数を 0.0 に足したり 1.0 にかけるなどして浮動小数点数に変換しておく)ことです。

最後は、送られてきた数値が float なら結果をそのまま返し(x is float ⇒ (⇑y))、結果が整数に近い値なら整数として返す(z > abs y - ☞x ← 1 * y + z ⇒ (⇑x) ⇑y)という余計で汚い処理(^^;)を付け加えています。

「spaceship」を解析するへ続く)

Smalltalk-72で遊ぶOOPの原点:「sqrt」を実装する(準備編)

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

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


まず abs を実装する

sqrt は、Smalltalk-71 の Spacewar ではここまで出てきた sqrt 3 としか使っていないので、1.73 を返す今の仮実装のままでも問題ないわけですが、何かのおりに使うかもしれませんので、この機会に書いておきます^^;

その前に、Smalltalk-72 には abs も無いのでこれをアクションとして用意します。Smalltalk-80 では abs は Number などのメソッドして用意されていますが、数値もメッセージとして送ることができ、かつ、継承の無い Smalltalk-72 では、アクション abs への数値の送信というスタイル(abs x)の方がなんとなくシンプルで良さそうです。

to abs x y (0 > :x ? (!-x) !x)

もちろん number など必要なクラス全てにメッセージ abs へ応答するためのメソッドを追加することで Smalltalk-80 以降のスタイル(x abs)にもできます。

既存のクラスへメソッドを追加するには addto アクションを使います。

addto number "(%abs ? (0 > SELF ? (!-SELF) !SELF))
addto float "(%abs ? (0.0 > SELF ? (!-SELF) !SELF))

ちなみに、Smalltalk-72 は - x はアクション - への x の送信です。ALLDEFS で to - を探すか、エミュレーター側で show - を評価することでその定義を見ることができます。なお、Smalltalk-72 で負数リテラル- ではなくオーバーバー̄(入力時はバッククオート)を使います。

繰り返しになりますが to - x ...xSmalltalk-71 のプロシージャの仮引数に似ていてもそれとは別で、たんなるテンポラリ変数宣言です。メッセージとして送られてきた数値は続いて括弧でくくられて記述されるメソッド本体冒頭の :x のところでフェッチ&アサインされます。

これもしつこいですが、:x もアクション: へのシンボル x の送信です。Smalltalk-72 はほぼすべてがメッセージングで記述されます。唯一の例外が条件分岐(式⇒(非偽時処理) 偽時処理)というのも、Smalltalk-80 と違っていて面白いですね。(いろいろ最適化している Smalltalk-80 ではメッセージでやっていないことは Smalltalk-72 より多いのですが、条件分岐は ifTrue: [] ifFalse: [] というメッセージを送信する、少なくとも“フリ”はしています。)

『sqrt』を実装する(コードと解説編)へ続く)

Smalltalk-72で遊ぶOOPの原点:スラスターの火炎を描く(再び謎解き&少しアレンジ編)

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

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


flame のあるべき実装を予測する

triangle が用意できたので、改めて Smalltalk-71 の flame を見ていきましょう。

スキャン画像を再掲します。

flame は、:size を引数にとるプロシージャ(Logo風に言うと。あるいは、Smalltalk-72風にはアクション)です。まず、軌跡が残らないようにペンを上げ(penup)、反対を向き(left 180)、2 + sqrt 3 だけ移動し(forward 2 + sqrt 3)描き始めるためにペンを下げています(pendown)。

はい。まずここ。ちょっと引っかかりますね^^;

:size を引数にとるこれまでのプロシージャの例に倣えば、forward 2 + sqrt 3forward (2 + sqrt 3) * :size の方がしっくりきます。

また、炎が描かれる場所までのタートルの移動と考えても、2 + sqrt 3 という中途半端な固定長より、宇宙船の胴体の半分の長さ、つまり (2 + sqrt 3) * :size 分だけ移動すると考える方が妥当です。

そのあとのtriangle size についても、size という返値有りプロシージャの存在という可能性も微レ存ながら、ここはやはり flame プロシージャが引数として得た :size を参照するのがスジだと思います。したがって:が抜けていると解釈します。

次の forward .5 については . をゴミと見ることもできますが、文字の間隔や次行の 1.5. と似た位置と大きさであることから、そのままの .50.5)が妥当と判断しました。 triangle 1.5 * :size の行の forward の後の 5 についても同様に . が消えた .50.5)でよさそうです。

最後の triangle 1 * :size の行の forward の後の空白の解釈は難しいですが、ここは素直に空白ととらえて forward :size と考えることにしましょう。

etc. は、ペンの位置と向きを元あった場所に戻す記述の省略と思われます。これは冒頭の penup の行の逆+α(triangle を描くために移動した分も戻す)を記述すればよいだけです。

したがって、あるべき Smalltalk-71 の flame の定義はこうなります。

to flame :size
  penup, left 180, forward (2 + sqrt 3) * :size, pendown
  triangle :size, forward .5 * :size
  triangle 1.5 * :size, forward .5 * :size
  triangle 2 * :size, forward .5 * :size
  triangle :size, forward :size
  penup, left 180, forward (2 + sqrt 3 + 2.5) * :size
end to

Smalltalk-72 へトランスパイルして動かしてみましょう。

to triangle size (
    @ pendn turn 90 go 0.5 * :size
        turn `120 go size
        turn `120 go size
        turn `120 go 0.5 * size turn `90
)

to flame size (
    @ penup turn 180 go ((sqrt 3) + 2) * :size pendn.
    triangle size. @ go 0.5 * size.
    triangle 1.5 * size. @ go 0.5 * size.
    triangle 2 * size. @ go 0.5 * size.
    triangle size. @ go size.
    @ penup turn 180 go ((sqrt 3) + 2.5 + 2) * size
)

disp clear
@ home penup goto 200 200 pendn. ship 30. flame 30

よい感じなのですが、炎の中の縦線がちょっと邪魔だし、最後の線分の長さも中途半端なので、ここは大胆(?)に改変して triangle後の縦線は描かないように penup を挿入することにします。

to triangle size (
    @ pendn turn 90 go 0.5 * :size
        turn `120 go size
        turn `120 go size
        turn `120 go 0.5 * size turn `90
)

to flame size (
    @ penup turn 180 go ((sqrt 3) + 2) * :size pendn.
    triangle size. @ penup go 0.5 * size.
    triangle 1.5 * size. @ penup go 0.5 * size.
    triangle 2 * size. @ penup go 0.5 * size.
    triangle size. '@ go size'.
    @ penup turn 180 go ((sqrt 3) + 1.5 + 2) * size
)

disp clear
@ home penup goto 200 200 pendn. ship 30. flame 30

[追記: よく考えると shippenup した状態で終わっているので、flametriangle もそれに倣っておけば、このアレンジは無用ですね^^; ]

to triangle size (
    @ pendn turn 90 go 0.5 * :size
        turn `120 go size
        turn `120 go size
        turn `120 go 0.5 * size turn `90
        penup
)

to flame size (
    @ penup turn 180 go ((sqrt 3) + 2) * :size pendn.
    triangle size. @ go 0.5 * size.
    triangle 1.5 * size. @ go 0.5 * size.
    triangle 2 * size. @ go 0.5 * size.
    triangle size. @ go size.
    @ penup turn 180 go ((sqrt 3) + 2.5 + 2) * size
)

disp clear
@ home penup goto 200 200 pendn. ship 30. flame 30

「sqrt」を実装する(準備編)へ続く)

Smalltalk-72で遊ぶOOPの原点:スラスターの火炎を描く(準備編)

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

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


プロシージャ triangle の定義を補う

次に、スラスター火炎を描く flame の Smalltalk-71 のコードを見てみましょう。

印刷のかすれ以外にも、いろいろ謎な部分がありますが、まずはキーになる triangle の定義が見当たらないので、次のように定義して補います。

to triangle :size
  pendown, right 90, forward 0.5 * :size
  left 120, forward :size
  left 120, forward :size
  left 120, forward 0.5 * :size, left 90
end to

処理の内容はこんな感じです。

プロシージャ triangle:size を引数に受け取り(to triangle :size ...)、まず念のため pendown してから、進行方向右横向きになり(right 90)半:size分 進みます(forward 0.5 * :size)。 そこから 60度の角を作るために 120度、つまり180-60度こんどは左向きに転回し(left 120:size分進む、を2回繰り返します。 最後に同じように角を作るために転回して(left 120)、元の位置に戻るために 半:size分進み、最後に進行方向左向きに転回し、元の向きに戻ります(left 90)。

これを Smalltalk-72 にトランスパイルし、実行して確かめましょう。

to triangle size (
    @ pendn turn 90 go 0.5 * :size
        turn `120 go size
        turn `120 go size
        turn `120 go 0.5 * size turn `90
)

disp clear
@ home penup goto 200 100 pendn. triangle 40

よさそうですね。

スラスターの火炎を描く(再び謎解き&少しアレンジ編)へ続く)