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 では組み込みのプロシージャを想定しているであろう ask を Smalltalk-72 で用意します。
![]()
to ask (disp _ :. !read eval)
すみません。かなり手を抜きました ^^;
この ask はメッセージとして送られてきた続く文字列を表示して、ユーザー入力の結果 read を Smalltalk-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