見出し画像

Retro-gaming and so on

12歳からはじめる ゼロからの Racketゲームプログラミング教室(大嘘) その4

4. まずはCLI版を作る

手始めにコマンドラインでのヴァージョンを作ろうと思う。
後にGUIに改造するにせよ、最初にCLIで作ってた方が見通しが良い、と思うからだ。
前回見た通り、Lispで作るノベルゲーム及びエロゲの基本システムはS式で構成されたリストであるシナリオのcarを返しcdrを残しておくだけ、だった。

;;; インタプリタにおけるeval
(define (interp exp scenario)
 (car scenario))
;;; ゲーム本体のプログラム(Read-Eval-Print Loop)
(define (repl scenario)
 (unless (null? scenario)
  (display (interp (read-char) scenario))
  (newline)
  (repl (cdr scenario))))

しかしながら、scenario.txt及びscenario-rkt.txtにはいくつかコマンドが用意されている。それらも再録しておこう。

  • back: 表示する背景画像を指定するコマンド
  • branch: 選択肢(分岐)を形成するコマンド
  • end: ゲーム終了を意味するコマンド
  • jump: 指定されたラベル(label)へと飛ぶコマンド
  • msg: テキストを表示するコマンド
  • putChar: 表示するキャラクタ画像を指定するコマンド
都合6つのコマンドである。メッセージを表示する「だけ」ならいざ知らず、他の5つのコマンドが流れ込んできた時、対処出来るようにしておかないとならない。そうなるとcarcdrだけだと事足りなくなるのだ。
現時点では画像情報は扱わない(CLIだから)前提だが、将来的にGUIに改造するため、基本的なコマンドは実装しておこうと思う。

さて、上の「もっとも単純なアドベンチャーゲームエンジン」あるいは「もっとも簡単な(何も出来ない)インタプリタ」であるinterpは二つ引数を持っている。
第一引数expは入力情報を受け取る引数で第二引数scenarioはシナリオを受け取る引数である。
で、だ。
実はプログラミング言語のインタプリタ実装ではこの第二引数に「環境」を取る事が多い。ここで言う「環境」とは、例えばビルトインの関数の集合だったり、あるいはインタプリタ上で定義した変数や関数の保存先の事である。
事実上、上の「超簡単な」ゲームエンジンでも、シナリオ自体はプログラミング言語インタプリタ実装での「環境」にあたる。事実、「現時点、ゲームのどこをプロセスしてるのか」と言う情報は「環境情報」に他ならない。
平たく言うと、後に扱うbig-bangで用いるworld構造体と言うのはこのインタプリタ上の「環境」を纏めて扱う「環境用データ」の事である。
従って、CLIのアドベンチャーゲームを作る前に、取り敢えず環境を保持するworld構造体を定義しよう。

;; World 構造体

(struct world
 (back branches characters color message scenario)
 #:transparent)

現時点、環境を構成してる要素は次の6つとしている。

  1. 背景画像の情報を保持するback
  2. 選択肢情報を保持するbranches
  3. キャラクタ画像の情報を保持するcharacters
  4. 選択された選択肢の色を決めるcolor
  5. 出力されるテキスト情報を保持するmessage
  6. シナリオデータを保持するscenario
なお、backmessageは文字列情報を想定してるがbranchescharactersscenarioはリストを想定している。そしてcolorは整数情報とする。
今のトコ、colorが何なのかピンと来ないだろうし、ましてやCLIのプログラムなので画像情報は全く関係がない。あくまで将来を見据えてこのようにしてるだけ、だ。
勘の良い人は、リストとして考慮された部分は「carしたりcdrしたりconsしたりする対象なんだろうな」と思うだろう。その通りだ。
なお、#:transparentと言うのはデバッグ時なんかで現在の構造体の中身を知る為に「透過」の意味で付けてあるオプショナル引数である。スケスケなのよ。いやんばか~んうふ〜ん。

では言語インタプリタではevalに当たる部分、world-goを実装する。world-goたぁヘンな名前だが、Racketのbig-bangのドキュメンテーションの記述に合わせただけ、である。
なお、余力のある人はWhiteSpaceのdoInstr関数Brainfuck関数の話を読んで来て欲しい。ぶっちゃけ、あちらはプログラミング言語処理系の話で、こっちはアドベンチャーゲームエンジンの話なのだが、やってるこたぁマジで全く同じなのだ。違うのはこっちはコマンドが6つ(+2)しかないから、あっちよりは構造自体は簡単だ、ってだけである。
では、world-go関数を紹介する。

(define (world-go exp w)
 (let loop ((back (world-back w))
     (branches (world-branches w))
     (characters (world-characters w))
     (color (world-color w))
     (message (world-message w))
     (scenario (world-scenario w)))
  (let ((s-expression (car scenario)))
   (let ((head (car s-expression)) (tail (cdr s-expression)))
    (case head
     ((back) (loop (car tail) branches characters
          color message (cdr scenario)))
     ((branch) (loop back (cons tail branches) characters
          color message (cdr scenario)))
     ((break) (cond ((or (char=? exp #\1)
             (char=? exp #\2))
          (world-go #\newline
           (world back '() characters
             color message
             (find-label
              (third
               (assv (digit-value exp)
                branches))
               scenario))))
            ((or (char=? exp #\y)
             (char=? exp #\n))
          (world-go #\newline
           (world back '() characters
             color message
             (find-label
              (second
               (assoc (list->string `(,exp))
                (map reverse branches)))
               scenario))))
            (else (world back branches characters
                color message scenario))))
     ((end) (world back branches characters
         color *end-message* scenario))
     ((jump) (world-go #\newline
        (world back branches characters
         color message
         (find-label (car tail) scenario))))
     ((label) (world back branches characters
         color message (cons '(break) scenario)))
     ((msg) (world back branches characters
         color (car tail) (if (char=? exp #\newline)
                (cdr scenario)
                scenario)))
     ((putChar) (loop back branches
           (if (member tail characters)
            characters
            (cons tail characters))
           color message (cdr scenario)))
               (else (error "Can't do " head)))))))

いきなり増えたな、オイ!」とか言うかもしんない。うん、増えた(笑)。ごめん(笑)。
最低限のアドベンチャーゲームエンジンや、何も出来ないインタプリタからすると雲泥の差、である。しかし、何らかのソフトウェアの「心臓部」なんつーのはこんなモンである(※1)。
重要なのは、関数world-goは入力(exp)とworld構造体を受け取って、world構造体を返す、と言う大枠である。つまり、ゲームの進行状態を受け取り、演算した後の進行状態を返す、と言う事である。まずはそれを押さえておこう。
あとはscenario.txt及びscenario-rkt.txtで使われてる6つ(+2)のコマンドが要請に従って分岐してるだけ、だ。それだけである。
では中身をザーッと見ていく。

まず最初のletで、与えられたworld構造体を要素別に分解している。これは演算で特定の要素に変更が起こり、またworld構造体を組み立て直す為に分解してるのだ。
なお、ここのletは単なるletではなく、名前付きletになっているが、その理由は後述する。
そして次のlet(car scenario)している(s-expressionと名付けた)。ここで常にscenarioの先頭のS式を取り出してるのだ。
また次のletでは(car scenario)carcdrに分解している(名称はheadtail)。car部分はscenario-rkt.txtの仕様の要請により必ずコマンド名になり、同様にcdr部分はコマンドの引数部分を纏めたリストとなる。
これで準備が整い、case構文で6つ(+2)のコマンドが要請してる演算を実行する。

backtail部が必ず背景写真のパスになっており、このコマンドが呼び出された時点で背景画像が差し替えられる。従ってworld構造体のback要素を書き換えれば良い。
ところで、それだけだとシナリオが進まないでback時点で止まってしまう。しかもここはキー入力が必要ない。狙うのはbackが実行された時点で「自動で」シナリオの次の先頭であるS式をぶっこ抜いて来る事、である。
冒頭のletが何故に名前付きletなのか、と言う理由はここにある。つまり、backが呼ばれた時点で自動で末尾再帰状態に突入し、別のコマンドが見つかるまでloopさせる、と言うのがポイントなのだ。それが理由でここでworld構造体のscenariocdrを取るわけである。

branchは選択肢候補であるtailworld構造体のbranchesリストにconsするのがその基本的役割である。ただし、これもbackコマンドと同様にキー入力で更新するわけにはいかない。ここでも望まれるのは「自動更新」であり、そのためここも名前付きletによるルーピング対象となっている。
また、Python原版ではコマンドの第四引数に"n"が見つかった時点で選択肢候補の読み込みを終了させてるようだが、Lispで、再帰を行う以上それは全く必要なくなってしまった。単純にインタプリタ(アドベンチャーゲームエンジン)がbranchから脱出した時点で選択肢の読み込み(cons)は勝手に終了する。よって"n"は全く役に立たないフラグになってしまったので、別の事に使おうと思う。

breakと言うコマンドはscenario.txt/scenario-rkt.txtには存在しない。このアドベンチャーゲームエンジン上「だけ」に存在する(※2)。これは選択肢を選ぶ際の処理コマンドとなる。
大まかに言うと、「選択肢を選ぶ」と言う行為ではbranchesリストにconsしたtailに含まれてる1、2及び"y"、"n"と言った情報を利用する(※3)。要するにbranchesリストを「連想リスト」に見立てるのだ。
例えば

> (define branches '((1 "遊びに行く" asobu "y") (2 "帰る" kaeru "n")))
> (assv 1 branches)
'(1 "遊びに行く" asobu "y")
> (assv 2 branches)
'(2 "帰る" kaeru "n")
>

である。
実際は返ってくる情報として必要なのはジャンプ先のラベル名である。
従って、基本は

> (third (assv 1 branches))
'asobu
> (third (assv 2 branches))
'kaeru
>

になる。
なお、入力関数としてはread-char関数を想定している。従って渡ってくる値はread-charの返り値である文字情報である。
最新Scheme仕様書では文字情報を直接整数に直すdigit-valueと言う超便利関数が用意されているが(※4)、生憎RacketはSchemeである事を止めたのでこれが用意されていない。
従って補助関数として自作するしかない。

(define (digit-value ch)
 (string->number (list->string `(,ch))))

つまりこういう事になる。

> (third (assv (digit-value #\1) branches))
'asobu
> (third (assv (digit-value #\2) branches))
'kaeru
>

ジャンプ先のラベルへと飛ぶので、ジャンプ用の補助関数、find-labelを用意する。これも簡単だ。

(define (find-label x scenario)
 (unless (null? scenario)
  (let ((s-expression (car scenario)))
   (let ((head (car s-expression)) (tail (cdr s-expression)))
    (if (and (eq? head 'label) (eq? x (car tail)))
     (cdr scenario)
     (find-label x (cdr scenario)))))))

例によって与えられたscenarioの先頭を調べてhead部とtail部に分解する。
headlabel命令で、ラベル名が引数xと一致した時、(cdr scenario)を返してルーピングを終了する。そうじゃなければscenario(cdr scenario)にして末尾再帰し、処理を継続する。
前回、大域変数*scenario*を作ったが、それを使って実験してみよう。

(find-label 'asobu *scenario*)


見た通り、(label asobu)以降のシナリオが返ってきてる。
つまり、選択肢の「選択」の一つの実装は次のようになるわけだ。

(find-label (third (assv (digit-value #\1) branches)) *scenario*)



これを一般化してbreakに分岐した時の、exp(入力)が#\1あるいは#\2の場合に対応させている。
もう一つのパターンはexp(入力)が文字#\yか#\nだった場合、である。
基本的には前と同じ方針なんだが、連想リストのキーが今度はtailのケツにある状態である。
と言う事はbranchesリストの要素を全部逆順にしてから検索しないとならない、と言う事だ。

> (assoc "y" (map reverse branches))
'("y" asobu "遊びに行く" 1)
> (assoc "n" (map reverse branches))
'("n" kaeru "帰る" 2)
>

この場合、ジャンプ先のラベルは返ってきたリストの第二要素となる。

> (second (assoc "y" (map reverse branches)))
'asobu
> (second (assoc "n" (map reverse branches)))
'kaeru
>

先程も書いた通り、read-charは文字を返すので、実際は文字から文字列へと変換しておかないとならない。

> (second (assoc (list->string '(#\y)) (map reverse branches)))
'asobu
> (second (assoc (list->string '(#\n)) (map reverse branches)))
'kaeru
>

find-label関数と組み合わせてみよう。

(find-label (second (assoc (list->string '(#\n)) (map reverse branches))) *scenario*)



このケースの場合(label kaeru)の後ろからのシナリオ全部が返ってるのが分かるだろう。
同様にこれをbreakコマンドの一つとして一般化して実装すれば良い。これらがbreakコマンドの内情の基本である。
そしてもう一つ注意点を言うと、選択肢が選ばれてしまえばもう選択肢は必要ないので、branchesスロットを空リストへと更新するようにしよう。
あとは少々のハックである(※5)。
breakの詳細を見ると末尾再帰が行われてるのに気づくだろう。しかも、名前付きletが設定されてるにも関わらず、大枠のworld-go関数自体を引数付き(#\newlineと言う文字)で呼び出して再帰している。一体これはどういうわけか。
実の事言うと、この構造はCLI版のテキストアドベンチャーゲームには必要がない。GUI版に必要な構造となってる。
具体的に言うと、find-label関数で目的のラベル以降のシナリオが設定されるわけだが、その時点でmsgコマンドが準備されるわけだ。ところがGUI版だとどういうわけかその状態にスムーズに進まず停止してしまう。
要するにその時点で、「無理矢理#\newline文字が入力された状態」として解釈させ、シナリオのS式を一個進めたい、と、そういうハックである。そのため、大枠のworld-go関数を引数(#\newline)付きで呼び出し、強制的にまずは一回、msgの中身を表示させようとしてるわけだ。
実際、気づく人は気づくだろうが、CLI版だと「余計な作業」に当たる為、ジャンプが実行された後はmsgが二行同時に表示されてしまう。しかし後にわかるだろうが、GUI版だと何の問題も無く一行だけ新たなメッセージが表示される。これが恐らくbig-bangのバグが絡んでる部分だと思われる。よってこれは暫定的な処置以外の何物でもない(※6)。


endコマンドは簡単である。単にworld構造体のmessage領域に終了メッセージを嵌め込むだけ、である。ゲームが終了したのでscenariocdr更新する必要もない。
そしてworld-go関数に於ける末尾再帰の脱出条件その1、でもある。

jumpコマンドは上で実装してるbreakコマンドの変種のようなモノである。基本的な仕組みはbreakと同じで、find-label関数を用いて目的のラベルの次からのシナリオ全体を返す。
そしてbreakコマンドと同様に、大枠のworld-goを引数付き(#\newline)で呼び出す事によって強制的にメッセージを進める。

labelコマンドは本来なら何もする必要がないのだが、ここではlabelが見つかった時点で現時点でのscenarioの先頭に'(break)命令をconsするようにしている。
ゲームシナリオの構造上、選択肢作成の後は必ずジャンプ先を設定しておかないとゲームが途切れてしまうわけで、ここで選択肢の選択状態(break)に入るように追加でscenarioに指令を書き加えるわけである。
ついでにいうと、jumpコマンドは直接ラベルの一つ先のS式に飛ぶように設計されてる為、言い換えるとlabel自体は調べるだけで、labelの中に入る事はないのだ。

msgコマンドがインタプリタ及びアドベンチャーゲームエンジンの基礎部分の流用である。read-charによる改行入力情報(#\newline)を受け取った時、world構造体のmessageスロットを(car tail)、つまりメッセージ目的の文字列で更新する。
また、scenario(cdr scenario)で更新し、次の入力に備える。そして改行以外の文字情報が入ってきた場合は単純に無視してゲーム進行情報(環境)をキープするのだ(※7)。



そしてmsg名前付きletによるルーピングの脱出条件その2、となっている。

putCharコマンドはbackコマンドやbranchコマンドの類である。自動でシナリオを進める為、名前付きletによる末尾再帰の対象になる。
そしてtailが画像に纏わる情報なので、そのままworld構造体のcharacterリストにconsしていくのが基本となる。
ただし、やってきた画像情報が既にcharacterリスト内に存在する場合、consせずに現時点でのcharacter情報を保持する事、とする(※8)。

これで終わりだ。
あとはゲーム本体のRead-Eval-Print Loop、REPLをでっち上げるだけ、である。
その前にちと、出力用関数printを作ろう。
今まで見てきた通り、インタプリタで言うトコの評価器(Evaluator)、あるいはevalであるアドベンチャーゲームエンジン、world-go関数はworld構造体を返すようになっている。
そして出力はメッセージか、あるいは選択肢さえ表示出来ればいいだけ、である・・・・・・取り敢えず今は画像情報は関係ないんでね。
つまり、出力用関数もworld構造体を受け取り、

  • world構造体のbranchesスロットが空リストの時は、messageスロットの内容を表示する。
  • それ以外の場合はbranchesスロットの内容を表示すれば良い。
って事になる。
このロジックで出力用関数を書くと次のようになる。

(define (print w)
 (display
  (format "~a~%" (if (null? (world-branches w))
          (string-replace (world-message w) "~%" "\n")
          (apply string-append
           (map (lambda (x)
              (string-append
               (number->string (car x))
               " "
               (second x)
               "\n"))
             (reverse (world-branches w))))))))

Racket組み込みのformatを使った上、基本的には整形出力に終始してるだけ、である。
一見メンド臭いところは選択肢を出力用に整形するトコだが、原則、branchesスロットはリストなんで、マッピングしながら必要な情報を文字列として纏めていってる。
あとは、シナリオファイルに記述されてる由緒正しいLispでの改行記号("~%")を一般的な改行記号("\n")に差し替えてるだけ、だ。

print関数をでっち上げられれば、REPLを作るのは屁の河童である。

(define (repl w)
 (print w)
 (if (game-ends? w)
  (exit)
  (repl (world-go (read-char) w))))

repl関数は末尾再帰でルーピングする関数である。
GUI版ではこの部分がbig-bangが担ってる場所なのだが、一応、ここではCLIでのインタプリタの動作原理をこうやって把握しておいた方が良いだろう。
World構造体wを受け取ったらprint関数が条件に従ってメッセージか選択肢を出力する。
以降は「ゲーム終了」状態だったらRacket自体を終了するexitを実行し、そうじゃなかったらキーボード入力により更新されたworld構造体の情報を引数としてrepl関数は末尾再帰する。そして、「キーボード入力によりworld構造体の情報を更新する」のがプログラミング言語インタプリタにおけるevalの役割で、ここでは先程作ったworld-go関数の役割なのである。

なお、ゲーム終了の判定関数game-ends?はまだ書いてない。
早速でっち上げよう。

(define (game-ends? w)
 (string=? (world-message w) *end-message*))

ゲームを本当に終えるかどうか、と言うのはworld構造体のmessageスロットが*end-message*と同じかどうか、調べるだけ、である。「おしまい」って書いてるのにゲームが続いたりしたらみっともないだろ(笑)?
そして大域変数*end-message*もまだ書いてない。ついでに初期メッセージの*init-message*も合わせて作っておこう。

;; 初期メッセージ

(define *init-message* "Enterでスタート")

;;; 終了メッセージ

(define *end-message* "終わり")

これで出来上がり、である。
次のようなrepl実行命令をRacketのリスナーに与えればCLI版のアドベンチャーゲームが開始される。

(repl (world #f '() '() 1 *init-message* *scenario*))



そして選択肢の「選択」は1、2、あるいはy、nをキー入力する事で実行出来る。



CLI版は以上である。
画像がないグラフィカルアドベンチャーゲームって状態だが、一応、プログラミング言語インタプリタの原理であり、アドベンチャーゲームエンジンの心臓部が一体どういう処理をしてるのか、と言うのを出来るだけ丁寧に説明してみたつもりではある。
で、マジな話、一回この「REPLを組み立てる」方法さえ覚えてしまえば後はバカの一つ覚えでソフトウェアを書くことが可能だ(※9)。実際問題、一回起動して何か実行して、そのまま終了してしまうスクリプトを除けば全てのソフトウェアはRead-Eval-Print Loopを持っている、と言って良い。例外はない。そして理論的な話をすると、GUIのソフトウェアでさえ例外ではないのだ(※10)。

さて、基本のアドベンチャーゲームエンジンの作り方をここで押さえておいて、次回からはいよいよbig-bangを利用したグラフィカルアドベンチャーゲームを作成していこうと思う。

なお、今回のCLI版アドベンチャーゲームの全ソースコードをここに置いておく。

※1: SICPPAIP(実用Common Lisp)では、このテの肥大化を防ぐため、データ駆動形プログラミング、と言う手法を提案している。平たく言うと、条件分岐を消して連想リストやハッシュテーブルを利用し、シンボルに対応した「計算」をそれらから拾ってくるプログラミングテクニックである。要するに「関数を小分けする」のだ。
その方が、記述量が減るわけではないが、見やすいと言えば見やすくはなる。

※2: 実の事を言うと、初期ヴァージョンではこれは存在しなかった。もっと言うとCLI版にも存在しなかった(初期ヴァージョンではlabel上で実装してた)。
実は「選択肢を選ぶ」と言う行為を実装した時点でbig-bangのおかしな挙動に悩まされたのだ。
「選択肢を選ぶ」時点で連想リストから結果を持ってきてジャンプするわけだが、動かしてみるとbreakと言う中間状況が存在しないとbig-bangが勝手にシナリオを進めるかあるいは何も進行しないか、と言う不可思議な挙動を目にしてて混乱したのだ。
恐らく、「無限ループ前提の」big-bangworld構造体の更新タイミングが合わない、と言う一種バグをbig-bangは抱えている。
結果、labelに突入した段階でシナリオの先頭にbreakコマンドをconsして一旦退避する、と言うのが更新タイミングを意図的にズラす事に繋がり、無事「選択肢を選ぶ」と言う行動を実装出来たのである。
一種のハックである。

※3: 前回書いたが、元々 #branch 命令の第一引数である1、2と言った数値は画像の位置情報の基本y座標に掛ける為の数学的な係数であり、第四引数の"y"や"n"と言った文字列はフラグであり、"y"は「続いて選択肢情報を読み込め」、"n"は「これを読んだら選択肢情報読み込みは終了せよ」を意図してた「らしい」。

※4: 実際、使う頻度が高い関数の割にはどのプログラミング言語でもこれが用意されていないので、快挙、と言って良い。
他の言語では毎回文字から数値48を引いたり、みっともない事をやっている(ASCIIコードでは文字0は数値48な為)。

※5: 元々、ハックとは別に「素晴らしいプログラムを書く」事ではない。本来はむしろ「やっつけ仕事」の意で、この文脈で使ってる意図も単なる「やっつけ仕事」である。
だからハッカーと言う単語も元来「素晴らしいプログラムを書く人」と言う意味ではなく、どっちかと言うと「やっつけ仕事的に大量にプログラムを書き殴る人」と言う意味なのだ。
大量にクズのようなプログラムを書き殴る人 -> 大量にプログラムを書くのでプログラミングの達人に違いない -> プログラムの達人、と短い間に意味が変容した言葉なのだ。
短い期間に貶した意図がほぼ逆の意味に転じた例であり、言語学上の研究材料としては極めて面白い「意味の変遷」の実例である。

※6: big-bangは全般的に見るととても良く出来たライブラリだとは思う。しかしながら、このテのバグが「発見・報告されていない」と言う事は、実際のトコ、Racketが人気があるLispの割には「誰もbig-bang自体を使っていない」と言う事だろう。
事実、big-bangを利用してゲーム作成する、と言うノウハウは、海外のサイトなりブログなりを検索しても全然存在しないのだ。ハッキリ言えば、この記事が「世界で初めてbig-bangを用いたゲームの書き方を指南した」ページになってる可能性さえある。
そして、これが意味するのは、やはりRacketのドキュメンテーションの「質の悪さ」であろう。要するに英語で書かれてるのはともかくとして、英語話者でさえ「必要な情報が書かれていない」と言う状態に遭遇してるんだ、と言う事実である。
Racket製作者陣は、正直言うと、猛省すべきである。

※7: 何か適当な文字を入力すると同じメッセージが複数回表示される、と言うような奇妙な現象に遭遇するだろう。
実はこれはバグではない。
read-charはキーボードから一文字入力を受け取る関数だが、仕様上、その後改行キーになるEnter/Returnキーを受け付けないと一文字を読み込まない。
従って、内部的には最低でも2アクションが必要になる。
そしてそれらの情報は「入力バッファ」と呼ばれる領域に一文字づつ保存されて先頭から消費されていくのだ。
つまり、適当な文字がインタプリタに入ってきた時点で「改行文字」自体は入力バッファに残り、そしてその後入力バッファからまたインタプリタに「改行文字」が自動的に流れ込んでくる。
このように、実は「一文字一文字」バカ正直に入力を考えると入力バッファとのやり取り、と言う面倒臭い現象に遭遇する。
そして一般的には一文字入力した時点で、本来だったら「入力バッファの消去」と言う作業が必要になるのだ。
ただ、ここでは最終目的がグラフィカルアドベンチャーゲームを作る事、なので、このくらいの不具合は無視しておく。

※8: 実はダウンロード出来るPython原作を見る限り、この辺の実装にバグがあるように見えるが、それに付いては後述しよう。

※9: もうちょっと理論的な話を知りたい、と言う人はSICP( 計算機プログラムの構造と解釈)の第4章を参照して欲しい。Web上で読むことが可能な、かつてMIT(マサチューセッツ工科大学)で使われてた教科書である。
そこではSchemeでScheme言語処理系を作ったりしてるが、このページで述べられてるworld-go関数とあまりに似通ったeval関数を組みたててる事に気づくだろう。
繰り返すが、アドベンチャーゲームエンジン作成と言語インタプリタ作成はかなり似通っているのだ。

※10: 実はWeb上で熱い話題であるGUI構成法のMVCデザインパターンの正体が何なのか、と言うと、ぶっちゃけて言えばCLIのソフトウェアでのRead-Eval-Print Loopに他ならない。しかし誰もそれを指摘しないのだ。
一つの理由として、意外とプロのプログラマでさえ、プログラミング言語実装の経験がある人が少ない、と言う事が示唆される。REPLの仕組みを知らない人たちは「MVCスゴい!」となってしまうのだろう。
もう一つ、ツールの功罪としては、初代Visual BasicやかつてのTurbo Pascalから始まって、RAD環境で簡単にGUIソフトウェアを書く事に人々が慣れすぎたせい、でもある。
RADはREPLの原則を無視して、グラフィカルインターフェースの「ガワ」に実行可能なスクリプトをペタペタ貼っていくような形式となっている。従って、コンピュータサイエンス的な「綺麗なプログラムを書こう」と言うスタイルからは元々逸脱していて、その影響を受けてる各種GUIツールキットはスクリプト群によるスパゲティコードを量産しつつイベントループを完全にプログラマから隠すような構造になっているのだ。
しかし、強調しておくが、MVCは取り立てて真新しい手法ではなく、初のGUIソフトウェアであるSmalltalkと言うプログラミング言語設計から採用されていた手法である。つまり、Smalltalkを開発した人たちは「Read-Eval-Print Loop」をむしろ熟知してたのである。
余談だが、一番最初のSmalltalkはBASICで実装されてたらしい。BASICでさえREPLを書ける、と言う事はREPLはソフトウェアを書く汎用テクニックなんだ、と言う証明に他ならない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事