Smalltalk-72で遊ぶOOPの原点:「ask」「start」の実装

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

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


ユーザー入力を受け付ける ask とゲームをスタートさせる start

いよいよ仕上げの start です。

まず、Smalltalk-71 では組み込みのプロシージャを想定しているであろう askSmalltalk-72 で用意します。

to ask (disp _ :. !read eval)

すみません。かなり手を抜きました ^^;

この ask はメッセージとして送られてきた続く文字列を表示して、ユーザー入力の結果 readSmalltalk-72 の式として eval してから返します。

これを使って start を実装しましょう。

to start ss pilot sy sx sbut (
    "SSIZE _ 6. 
    "MOVELAG _ "FRAMELAG _ 0. 
    "SPSCALE _ 1.0. "DIRSCALE _ 1.0. "LSCALE _ 1.0. 
    "CLOSE _ SSIZE * 3. 
    "TORPLIFE _ 2000.
    spacewar delete all.
    spacewar schedule keysens.
    stick delete all.
    do ask 'how many will be playing~ ' (
        "pilot _ ask 'pilot''s name str~ '.
        "sy _ stick ask 'two chars (keys) str for stick y-axis~ '.
        "sx _ stick ask 'tow chars (keys) str for stick x-axis~ '.
        "sbut _ stick ask 'one char (key) str for stick button~ '.
        "ss _ spaceship pilot sy sx sbut.
        spacewar schedule ss)
    disp _ 'type ''esc'' to exit...'.
    spacewar run)

"disp _ dispframe 16 480 514 184 string 2000. disp clear
@ erase. disp display.
start

作業用に広げていたターミナルウインドウのサイズを元に戻して( disp ← ... )から、画面を綺麗にして( ☺ erase. disp display.start を実行します。

すると、プレイヤー数、プレイヤー名、推進力操作のキーのペア、操舵のキーのペア、魚雷発射キーをどうするかについて1プレイヤーずつ訊ねられるので、プレイヤー数については整数、それ以外は文字列リテラル'...' )でタイプして do-it \(グリフは )することで入力できます。

全ての入力が終えると escキーで中止できる旨のメッセージを表示してゲームが始まります。

おわりに

当初の甘い計画では 18回くらいでサクッと終わらせて、残りは Smalltalk-72 らしい実装に試みにあてたかったのですが、ままならないものですね…^^;

ともあれ、なんとか start でゲームを開始するところまでこぎ着けられてよかったです。

おそらくオブジェクトのアクティベートのタイミングへの理解が足りていないのが主な理由でしょうが、“原因不明”のエラーとの格闘で無駄に時間を溶かしてしまいました。しかし、おかげでメッセージング のみ によるプログラミングを今まで以上に踏み込んで体験し身につけられたように思います。

一方で、クラスやオブジェクトがクロージャーで実現されていて、メッセージを受け取るために「アクティベート」と称する実行が必要になる Smalltalk-72 がその非効率さ以外にも抱えている構造的な問題点もなんとなくいろいろと見えてきたような気がします。

今後は、前述のとおり今回果たせなかった Smalltalk-72 ならでは版の他に、Squeak や Pharo といった現在の Smalltalk で実装したらどんなふうになるかも試してみたいと思っています。

また、オリジナルのスペースウォー・ゲームの仕様を探して、Smalltalk-71版のコードは何が違うのか、といったあたりも調べてみたいです。

Smalltalk-71 についてはますます謎が深まっただけで終わってしまったような残念な感じではありますが、それでも、なるほど「A Persona Computer for Children of All Ages(あらゆる年齢の『子供たち』のための パーソナルコンピュータ)」 でジミーとベスが遊んでいたのはまさにこのSmalltalk-71で書かれたスペースウォー・ゲームそのものであり、この論文(エッセイ?)にアラン・ケイ氏が掲載しようとしていたのもまさにこの Smalltalk-71版のコードだったのでは!?…という今更ながらの気付きを得ることができました。この予想が当たっているとしたら、読み解くのにかなり労力を費やした身としては、ダニエル・G ・ボブロー氏の助言はしごくまっとうなものだったと強く同意します^^;

あと非常に気になった点として、これはオリジナルの当ゲームの仕様がどうだったか次第で意見が分かれるところではありますが、このゲームでは向きの操舵が慣性に従っていないところがずっと引っかかりました。速度と同様に宇宙船の向きを制御するそれ用のスラスターを用意し、加速・減速を意識した姿勢制御がしたいところです。

グラフィック出力やキー入力などの IO を手軽に扱えることが前提ですが、新しく学ぶ言語や処理系を試すときにこのスペースウォー・ゲームはほどよい規模の題材として使えそうです。今後も大いに活用してゆこうと思います。

付録1:Smalltalk-71 のコードを極力修正したバージョン

Smalltalk-72 への移植を通じて、意味が通りにくい部分について恐らくこうなのではないかと修正を試みた Smalltalk-71版のコードです。

to ship :size
  penup, left 180, forward 2 * :size, right 90
  forward 1 * :size, right 90
  pendown, forward 4 * :size, right 30, forward 2 * :size
  right 120, forward 2 * :size
  right 30, forward 4 * :size
  right 30, forward 2 * :size
  right 120, forward 2 * :size
  left 150, forward :size * 2 * sqrt 3.
  left 150, forward :size * 2
  right 120, forward :size * 2
  left 150, forward :size * 2 * sqrt 3
  penup, left 90, forward :size, right 90, forward 2 * :size
end to

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

to flash
  etc.
end to

to retro
  etc.
end to

to torp
  etc.
end to

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

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

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

to ?crash :object
  find all (create spaceship :s)
    if :object ≠ :s
      and |:object:location:x - :s:location:x| < :close
      and |:object:location:y - :s:location:y| < :close
    then explode :s, explode :obj
end to

to explode :object
  penup, moveto :object:location
  flash
  finish :object
end to

to torpedo :speed :direction :location
  use :thrust 0
  bump :numtorps
  moveship
  if not (0 < :location:x < 1024 and 0 < :location:y < 1024)
  then ?bump :numtorps, finish :self
end to

to start
  repeat ask "how many will be playing?" times
    create spaceship ask "pilot's name?"
      stick.(make :sn be ask “stick number?”).y
      stick.:sn.x
      stick.:sn.but
  end repeat
  if (make :char be ask) = “s” then done
  find all (create spaceship :x)
    start :x
end to

*start
how many will be playing?
*2
pilot's name?
*Jimmy
stick number?
*2
pilot's name?
*Bill
stick number?
*3

付録2: Smalltalk-72版 全コード(Smalltalk-72エミュレータのSnippetsウインドウへのコピペと二回クリックによるターミナルへの転送用)

"disp _ dispframe 16 480 8 670 string 2000. disp clear

to ship size (
    @ penup turn 180 go 2 * :size turn 90
        go 1 * size turn 90
        pendn go 4 * size turn 30 go 2 * size
        turn 120 go 2 * size
        turn 30 go 4 * size
        turn 30 go 2 * size
        turn 120 go 2 * size
        turn `150 go (sqrt 3) * 2 * size
        turn `150 go 2 * size
        turn 120 go 2 * size
        turn `150 go (sqrt 3) * 2 * size
        penup turn `90 go 1 * size turn 90 go 2 * size)

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

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

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

"PI _  3.14159265

to nfact acc n m (
    7 < :m ? (error "(16 bit signed int overflow))
    "acc _ 1. for n to m do ("acc _ acc * n) !acc)

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 + :)

to retro (@ turn 180)

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

to clock (!mem 280)

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

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

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

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

to stick x y i kcode : keys val : kmap (
    %delete ? (%all ? ("kmap _ nil)
        :#x.
        for i to 256 do ("y _ kmap[i]. eq #x #y ? (kmap[i] _ nil)). )
    (null kmap ? ("kmap _ vector 256))
    %process ? ("x _ kmap[1+:kcode].
        null #x ? (!false)
        x handle kcode. )
    isnew ? (
        (1 = :keys length ? ("val _ false) "val _ 0).
        for i to keys length do (kmap[1+keys[i]] _ #SELF))
    %print ? (disp _ '(stick '. disp _ 39. disp _ keys. disp _ 39. disp _ ') ')
    %handle ? (
        :kcode.
        1 = keys length ? ("val _ true)
        1 = keys[1 to 2] find first kcode ? ("val _ val - 1) "val _ val + 1)
    eq val true ? ("val _ false. !true)
    !val
)

to keysens (
    %step ? (repeat (kbck ? (stick process kbd) done))
    #keysens)

to spacewar x y : : objects (
    (null objects ? ("objects _ obset))
    %schedule ? (objects _ :#)
    %delete ? (%all ? ("objects _ nil) objects delete :#)
    %run ? (
        repeat (
            objects do (null each ? () each step).
            1 = objects vec length ? (done)))
    %find ? (%all. :"x. "y _ obset.
        objects do (each is~ = x ? (y _ each)).
        !y))

to crash object other (
    %~. "object _ :#.
    spacewar find all spaceship do (
        "other _ each.
        eq #object #other ? ()
        CLOSE < abs (object locx - other locx) ? ()
        CLOSE < abs (object locy - other locy)? ()
        explore object. explore other))

to explore object (
    :#object.
   @ penup goto object locx object locy.
   flash.
   finish object
)

to flash (
    do 10 (
        @ penup turn 36 go SSIZE * 2. 
        @ pendn triangle SSIZE * rand between 2 5.
        @ penup turn 180 go SSIZE * 2 turn 180))

to finish obj stk (
    :#obj.
    obj release.
    spacewar delete obj)

to torp size (
    @ penup turn 180 go :size turn 90
        go 0.5 * size turn 90
        pendn go 2 * size turn 30 go size
        turn 120 go size
        turn 30 go 2 * size
        turn 30 go size
        turn 120 go size
        penup turn 120 go 0.5 * size turn `90 go size)

to torpedo : thrust steer locx locy speed direction time
        ftime llocx llocy ldir lthr launcher endlife (
    isnew ? (:launcher. :speed. :locx. :locy. "ldir _ :direction.
        "locx _ "llocx _ locx + (cos direction) * SSIZE * 10.
        "locy _ "llocy _ locy + (sin direction) * SSIZE * 10.
        launcher bumptorps.
        "thrust _ "lthr _ "steer _ 0. 
        "time _ "ftime _ clock.
        "endlife _ clock + TORPLIFE)
    %release ? (
        stick delete thrust.
        stick delete steer.
        stick delete trigger)
    %locx ? (!locx)
    %locy ? (!locy)
    %step ? (
        0 < clock - time + MOVELAG ? (
            "time _ clock.
            0 < clock - endlife ? (
                launcher debumptorps.
                display torp erase.
                finish SELF)
            moveship.
            crash~ SELF.
            display torp))
    %is ? (ISIT eval)
)

to display obj (
    :#obj.
    0 < clock - ftime + FRAMELAG ? (
        "ftime _ clock.
        @ penup goto llocx llocy up turn ldir + 90 pendn white.
        obj SSIZE.
        (0 < lthr ? (flame SSIZE)
        0 > lthr ? (retro flame SSIZE)).
        @ penup goto locx locy up turn direction + 90 pendn black.
        %erase ? ()
        obj SSIZE.
        (0 < thrust ? (flame SSIZE)
        0 > thrust ? (retro flame SSIZE))
        "llocx _ locx. "llocy _ locy. "ldir _ direction ."lthr _ thrust))

to spaceship newtorp : pilot thrust steer trigger numtorps locx locy speed direction time
        ftime llocx llocy ldir lthr (
    isnew ? (:pilot. "lthr _ :#thrust. :#steer :#trigger. 
        "numtorps _ "speed _ 0.
        "direction _ "ldir _ 0 + rand * 360.
        "locx _ "llocx _ rand between 50 462.
        "locy _ "llocy _ rand between 50 462.
        "time _ "ftime _ clock)
    %release ? (
        stick delete thrust.
        stick delete steer.
        stick delete trigger)
    %locx ? (!locx)
    %locy ? (!locy)
    %step ? (
        0 < clock - time + MOVELAG ? (
            "time _ clock.
            (trigger ? (3 > numtorps ? (
                "newtorp _ torpedo SELF speed locx locy direction.
                spacewar schedule newtorp))
            moveship.
            crash~ SELF.
            display ship)))
    %is ? (ISIT eval)
    %bumptorps ? ("numtorps _ numtorps + 1)
    %debumptorps ? ("numtorps _ numtorps - 1)
)

to ask (disp _ :. !read eval)

to start ss pilot sy sx sbut (
    "SSIZE _ 6. 
    "MOVELAG _ "FRAMELAG _ 0. 
    "SPSCALE _ 1.0. "DIRSCALE _ 1.0. "LSCALE _ 1.0. 
    "CLOSE _ SSIZE * 3. 
    "TORPLIFE _ 2000.
    spacewar delete all.
    spacewar schedule keysens.
    stick delete all.
    do ask 'how many will be playing~ ' (
        "pilot _ ask 'pilot''s name str~ '.
        "sy _ stick ask 'two chars (keys) str for stick y-axis~ '.
        "sx _ stick ask 'tow chars (keys) str for stick x-axis~ '.
        "sbut _ stick ask 'one char (key) str for stick button~ '.
        "ss _ spaceship pilot sy sx sbut.
        spacewar schedule ss)
    disp _ 'type ''esc'' to exit...'.
    spacewar run)

"disp _ dispframe 16 480 514 184 string 2000. disp clear
@ erase. disp display.
start