0. Preface
実はこのシリーズ(って程でもないが)は2回で終了するつもりだった。
しかし、Racketの2htdp/universe及びbig-bangでの画像書き換えのノウハウを作って纏めておいた方が良いのでは、と考えた。
そんなわけで、「12歳からはじめる ゼロからの Pythonゲームプログラミング教室」の第8章で扱われてる(らしい・※1)アドベンチャーゲームをRacketでプログラムするにはどうすれば良いのか調べていた。
いやこれが大変だったんだ・・・・・・。元々big-bangと言うツールは簡単なアクションゲーム制作しか念頭に置いていない。かつ、前にも書いたけど、GUIと言うのは無限ループ前提なわけだが、お陰さんで、こっちが狙ってるデータの更新タイミングとbig-bangの設計方針にズレがあるらしく、CLIで上手く行ってるプログラムがGUIだと上手く行かない・・・なおかつ、何度も言うが、見た目は豪華だがRacketのドキュメンテーションはどっちかっつーと最悪の部類に入る(※2)。例示はねぇわ詳細が分からないわ、ってぇんで徒手空拳で「挙動」を調べるしかなかったのだ・・・・・・しかも、風邪をひいてたせいで、頭脳的にも体調も最悪の二週間くらいだった(苦笑)。
まぁ、そんな事もあってブログの更新もサボってたわけだが。
色々弄ってたお陰でようやくbig-bangを使った簡単なアドベンチャーゲームの作り方が分かった。
と言うわけで、ここでRacketを使った簡単なアドベンチャーゲームの作り方を紹介したいと思う。
なお、前にも言ったが、ここで使う画像データは「12歳からはじめる ゼロからの Pythonゲームプログラミング教室」の公式ページから取ってこれる。
提供されてるデータは12saipythonと言うダウンロードフォルダに入ってる。今回使うのはその中のimg8と言うフォルダ、及びそれに含まれている画像データとシナリオデータである。適当なワーキングディレクトリにimg8をコピーしておいて欲しい。
さぁ、始めようか・・・・・・と言いたいトコなんだけど、最初にいくつか能書きを垂れておこうと思う。
1. アドベンチャーゲームと言う教材
前にも書いたが、実のトコ、アドベンチャーゲームと言うジャンルのゲームは歴史的には真っ当なコンピュータ・サイエンスの落とし子である。
従って、真面目な話、「アドベンチャーゲーム」を一から作る、と言うのは極めてコンピュータサイエンス的なトピックになるのだ。・・・・・・不思議な事に、どうやら大学等ではこれを使って「教える」と言う事をやってないらしい。非常に勿体無い話である(※3)。
アドベンチャーゲーム作成には与えられたシナリオの「字句解析」及び「構文解析」と言う技術が必要となる。つまり、アドベンチャーゲームエンジン作成は、「大学で教える」インタプリタ作成やコンパイラ作成と極めて近いトピックを扱っているのだ。
何だか分からんまま、言われるままに「字句解析」や「構文解析」の宿題をやるより、絶対アドベンチャーゲーム作成の方が「楽しい」だろう。エロゲのすぐ横にはコンピュータサイエンスが横たわっているのである。いや、マジな話で、だ。
なお、ダウンロードした12saipythonにはchap8.pyと言うソースコードが入ってると思う。実はそのプログラムのdecode_lineと言う関数だが、これが上で書いてる「字句解析」及び「構文解析」のプログラムである。img8と言うフォルダに含まれてるscenario.txt・・・・・・事実上、「12歳からはじめる ゼロからの Pythonゲームプログラミング教室」におけるアドベンチャーゲームエンジン用の「プログラム」が書かれた内容を「解釈」して「実行」するようになっている。
そう、実はアドベンチャーゲームエンジンを作る、と言うのはある意味「プログラミング言語を自分でデザインする」のと同じなのだ。アカデミックだろ(笑)?
実際、今人気のアドベンチャーゲーム作成言語、例えばnscripter(※4)とか吉里吉里Zなんかもコンピュータサイエンス的な作品である。アドベンチャーゲーム作成を極めようとすると、必ずと言って良い程、インタプリタやコンパイラ作成にツッコんでいくのである。
これ程効率良くコンピュータサイエンスの世界にツッコんでいける教材はあまりないのではないだろうか。
2. Back to the BASIC
Land of Lispと言う書籍でも最初の段階でテキストアドベンチャーゲームを題材として取り上げてる。そして初歩的なグラフ理論と言う数学の一分野を扱ったりしてるわけだ。
ただ、これはコマンド入力によるテキストアドベンチャーゲーム「ならでは」の複雑さ、と言って良いだろう。何故なら情景描写も全てテキストで行われる為、単一方向へ「進む」のではなく、「語られる事象」同士がリンクを貼ってるような構成になってないとゲームにならないから、である。
一方、今日のグラフィカルな、いわゆるノベルゲームやらエロゲと言われるジャンルのゲームだとそこまで複雑な「データ同士のリンク」は基本必要ない(※5)。何故なら、「情景描写」は全てグラフィックが担ってるからだ。そしてプログラミングに関する「複雑な部分」は全てグラフィック処理に移ってるので、シナリオ自体は基本一方通行で構わない。
従って、作成するノベルゲーム、ないしはエロゲのエンジンが「解釈」するシナリオによるプログラムは、往年のBASICのような一方通行であるバッチ式の逐次処理型のプログラミング言語となれば良い、ってのが原則になる。
つまり、大まかに言うと、nscripterや吉里吉里Zのような「アドベンチャーゲーム作成用」プログラミング言語は多かれ少なかれ、基本はBASICのようなバッチ型のプログラミング言語となっているのだ(※6)。
我々が普段使ってるプログラミング言語は、往年のBASICやFortranのようなバッチ式のプログラミング言語ではなく、構造化プログラミング言語に始まってオブジェクト指向プログラミング言語やら関数型プログラミング言語等、機能的には複雑なプログラミング言語である。それらの方がプログラムを書くのが簡単だから、だ。
しかしここに至って、アドベンチャーゲームエンジンだと、何故か往年のBASICのようなプログラミング言語を書いた方がラクだ、って事になってしまってる。巡り巡って旧いスタイルのプログラミング言語の方が「良い」って状況になってしまった。おかしくないか。
いや、実はおかしくないのだ。
これはノベルゲームやエロゲの、ある種の特殊性が招いた事態である。と言うよりも、フツーの小説や漫画でもそうだ。基本これらのメディアは「逐次性」が前提なのである。
つまり、我々は、例えば漫画を読む時、「最初のページから順番に」読んでいく。決してページの途中から読んだり3ページ目の4コマ目から72ページ目の8コマ目にいきなり飛んだりはしない。
あるいは、キャラ設定だけ先にあって、それを一々参照しながら読んだりもしない。
例えば
class ヒロイン:def __init__(self, 好感度):self.好感度 = 好感度class 喜多川海夢(ヒロイン):def コスプレ(self, 衣装):return 着替えdef ギャル語(self, 日本語):return ギャル語(日本語)def love(self, 相手):return しゅきしゅきしゅき♥
みたいにキャラの雛形を設計して、何かの動作があった場合、メソッドに定義された行動を「返す」って言うような様式の漫画があったとしたら読んでるこっちは困っちゃうだろ(笑)。時系列もヘッタクレもあったもんじゃない(※7)。
そう、「物語の記述」と言う枠組みは「時系列」なので、従って「逐次処理」と言う方式が相性が良いわけだ。
ここでちとLispに戻ろう。
例えばシナリオらしいシナリオたぁ言えないが次のようなリストがあったとする。
(define *scenario* '("星田オステオパシー: 僕のブログのアドレスは https://blog.goo.ne.jp/hosidaosuteo だよ!\n"
"M太郎: 僕のブログのアドレスは https://blog.goo.ne.jp/blue_031 さ!\n"
"ハリソン: ワタシノブログノアドレースワ https://blog.goo.ne.jp/harrison2018\n"
"isam: 私のブログのアドレスは https://blog.goo.ne.jp/isamrx72 です。\n"))
たった4行のアドベンチャーゲームとも呼べないようなシナリオだ。
しかし、重要な示唆としては、ノベルゲームやエロゲはグラフィックや音楽を抜けば原則次のようなプログラムなのである(※8)。
;;; インタプリタにおけるeval(define (interp exp scenario)
(car scenario));;; ゲーム本体のプログラム(Read-Eval-Print Loop・※9)(define (repl scenario)
(unless (null? scenario)
(display (interp (read-char) scenario))
(newline)
(repl (cdr scenario))))
Lispではたかだか10行に満たないプログラムがノベルゲーム、あるいはエロゲの本体なのだ。システム的には受け取ったリスト(シナリオ)のcarを返し出力し続け、なおかつ本体にはリスト(シナリオ)のcdrを残していく。
実際次のようなワンライナーを書いて実行し、リターンキーを叩き続ければ*scenario*に含まれてる文字列が一行づつ表示されていくだろう。
(repl *scenario*)
繰り返すが、この単純なプログラムがノベルゲームあるいはエロゲと呼ばれる類のゲームの基本である。
実際はまだ選択肢によるジャンプを実装してないが、いずれにせよこの基本を押さえておいて欲しい。グラフィカルなノベルゲームやエロゲではテキストアドベンチャーゲームにおけるグラフ理論もクソも関係ないのだ。少なくともLispではリスト操作の基本、carとcdrだけで90%は実装出来るのがノベルゲーム/エロゲなのである(※10)。・・・・・・絵や音が無ければ、ね。
いずれにせよ、これがグラフィカルアドベンチャーゲーム作成の基本であり、と同時にプログラミング言語処理系作成の基本でもある。ウソのようなホントの話、だ。
ここから徐々に「自分独自のプログラミング言語作成の道」がはじまるのである。
・・・と言いたいとこだがその前に。
3. S式とマークアップ言語
かつて著名なLisperであるポール・グレアムのとある発言に噛み付いたPythonistaがいた。
ネタ的に抜粋するとこれである。
実行時にコンパイルするというのはLispをEmacsのようなプログラムの拡張言語として 使うことを容易にする。そして、実行時に読み込みができるというのは、 プログラム同士がS式を使って通信できるということだ。 最後のアイディアは最近XMLとして再発明された。---ポール・グレアム
Lispは時代のはるか先を行っていた。 Lispは最初の高級言語だったから、それ以降に現れた良いものは全てLispの再発明だ、 と信じたくなる人々がいる。 JavaはLispだ。XMLもLispだ(違うのだが)。---ポール・プレスコード
ここで注目すべきはXMLである。
XMLは大雑把に言うと色んなフリーソフトの設定ファイル等に使われてる「文書記述形式」である(※11)。
XMLの細かい話は置いておくが、この両者の発言、どっちが正しいのだろうか。実は歴史的に見ると、XMLに関しては後者のポール・プレスコードの発言の方が正しい。正しいんだが・・・・・・。Lispを使う人から言わせるとポール・グレアムの発言も心情的には良く分かるのだ。
と言うのも、XMLに含まれるMLの部分に秘密がある。
MLとはマークアップ言語、の略称である。マークアップ言語とは適当なテキストを何らかの指示子等で挟んで「装飾しながら」記述する方式の事を指す。XMLを知らない人でもHTMLは知ってるだろう。ブラウザで解読する為の文書フォーマットである。
例えば一番簡単なHTMLの記述は次のようなモノだ。
ご覧のように、表示したいモノを「指令」で挟み込むカタチになっている。
この形式の特徴というのは、パーズしやすい、と言う辺りにある。
例えばWebブラウザではHTMLを「解釈」して表示するわけだが、文書の装飾を紛れなくやるためには「構文解析」しやすさが重要となる。ややこしい構文解析を要求するような形式なら、素早いレスポンスが必要なWebブラウザには向かないのだ。
しかし、Lispを知ってる層だと、「構文解析のしやすさ」を重視するなら、Lispで使われてるS式(Symbolic Expression)・・・要するに「カッコで挟まれた式」で充分じゃないの?と思ってしまうのだ(※12)。しかもどう考えても、手書きをするならS式の方が、HTMLの形式より「マシ」だろう。
仮に、HTMLがS式だったら、上のHTMLは次のようになってるだろう。
(!doctype html
(html
(head
(title タイトル)(body(p 簡単な例)))))
どうだろうか?明らかに素のHTMLより記述量は少ないと思わないだろうか(※13)。
Lispは「カッコばかりで腹が立つ」と言う人も多いが、反面、XMLやらHTMLやらのマークアップ言語よりは記述要素が少なくシンプルに記述できるのは事実なのだ(※14)。
ポール・グレアムが本当に言いたかった事は恐らく
「既に優秀なパーズしやすいデータ形式(S式)があるのに、なんでわざわざ新しい形式を"発明"する必要があるんだ?」
と言うようなものだろう。そして多かれ少なかれ、Lisp使いは似たような感想を持っている。
データにもコードにもなるS式以上の「便利な形式」はあり得ない、と。
なんでこんな事をここで書いてるのだろうか。
と言うのも、アドベンチャーゲームのシナリオの「形式」をどうするか、と言う話をしようとしてたから、である。
シナリオは狭義では単なるテキストファイルであるが、アドベンチャーゲームエンジンに食わせる前提で話をすると、実は立派な「プログラミング言語で記述されたプログラム」である。言い換えると狭義では単なるデータ、だがアドベンチャーゲームエンジンに対しては「プログラム」なのだ(※15)。
で、だ。
上の方で「ADVエンジンを作ると言う事は、プログラミング言語を作る、と言う立場に極めて近い」と言う話をしたんだけど。
もちろん「自作プログラミング言語を作る」事自体は素晴らしいんだが、今このトピックに於いては「やめといた方がいい」と言う事を言いたかったのだ(笑)。
実際、面倒なのは、プログラミング言語の「実装」より「設計」なのだ。たとえアドベンチャーゲーム実装目的でも無矛盾なプログラミング言語を「まずは紙の上で」設計するのには手間がかかる。
そして、データとして見た場合、自分でデータ形式を「設計」するよりはXMLなんかのマークアップ言語を転用した方が良さそうではあるんだが、だったらS式を直接使っちまった方がラクだ、と言う事だ。なんせここではアドベンチャーゲームをLispで書こうとしてるんだし。
ハッキリ言うと、フツーのアドベンチャーゲームの「シナリオ記述形式」でさえ、S式以上に上手い案は実際のトコ存在しないのだ。そしてLisp使いはそれをすぐ思いつく。Lispを知らない人たちは自作言語にこだわる。
Lispを知ってる人たちとLispを知らない人たちとの差、というのはこの辺なのだ。Lisp使いはこのテの無駄を嫌う。
もう一つ、LispでS式を用いたシナリオを読む事についてはメリットがある。Lispのread関数を使えば字句解析等の手間がかからない。何度も言うがLispのread関数はPython等のinputと違いむき出しの構文解析器である。従って、S式の形式だったら読み込みに於いては全く引っかからないのだ(※16)。
これが、例えばXMLやJSON等を利用しようとすると、字句解析器や構文解析器を全部作らないとならない。もちろん言語によってはライブラリが用意されてるだろうが、Lispのread関数のように「必ずある」わけではない。
言い換えるとこの辺も「面倒」なのだ(※17)。
ところでimg8フォルダに含まれるscenario.txtを読んでみて欲しい。これが「自作言語で記述された」プログラムの例、となる。
ザーッと読んでみて分かるのは
- キャラクタが喋る「セリフ」は地のテキストである。
- 語頭が#になってる単語は、プログラムでの「命令/関数」に当たり、後続する表現は関数の「引数」に当たる。
- 語頭が## になっている単語はジャンプ先の「ラベル」を意味してて、後続する表現は「ラベル名」を意味してる。
と言う事だ。
そして各種命令/関数は次のようになってるらしい。
- #back: 一引数関数。引数はバックグラウンドに使用する絵のパスである。
- #putChar: 二引数関数。第一引数はキャラクタの絵のパスで、第二引数は表示位置である。表示位置はL(左)、C(中央)、R(右)の三種類で指定する。
- #branch: 四引数関数。第一引数は選択肢の位置情報にまつわり、具体的には位置情報に掛ける係数である。第二引数は選択肢の表示内容、第三引数はその選択肢が選ばれた際のジャンプ先、第四引数は選択肢をADVエンジンに読み込み続けるか否かを指定してるらしい。"y"は読み込み継続、"n"は「ここで読み込みは終了する」を意図してる模様。
- #jump: 一引数関数。ジャンプ命令で、引数で与えられたラベルへとジャンプする。
- #end: 無引数関数。ゲーム終了命令である。
まぁ、ハッキリ言って「多分」である。なんせ僕はこの本を読んでないからソースコードから想像するしかないのだ。特に選択肢表示に纏わる#branch命令の1とか"y"とか、初見では一体何を目的にしてんだかサッパリ分からんかったくらいだ。
さて、scenario.txtの内容をS式として書き換えたいわけだが・・・・・・手書きはやめよう。
ぶっちゃけ、scenario.txtが「構文解析をキチンと行える」設計になってるなら、トランスレータを書いて一気に翻訳してしまえば良い。
そのコードは次のようになる。
#lang racket
(require (only-in srfi/13 string-tokenize))
(define (translator some-file)
(with-input-from-file some-file
(lambda ()
(let loop ((line (read-line)) (acc '()))
(if (eof-object? line);;; ファイル書き出し
(with-output-to-file;;; ファイル名に-rktを添付する
(string-replace some-file "." "-rkt.")
(lambda ()
(let loop ((ls (reverse acc)))
(unless (null? ls)
(display (car ls))
(newline)
(loop (cdr ls))))))
;;; 一行づつ読み込んで処理していく
(loop (read-line)
(cons;;; ここからscenario.txtの仕様に従ってS式に変換していく
(if (char=? (string-ref line 0) #\#)
(if (char=? (string-ref line 1) #\#)
(format "(~a)" (string-trim
(string-replace line "##" "label")))
(let ((ls (string-tokenize
(string-trim (substring line 1)))))
(case (string->symbol (car ls))
((back) (format "(~a ~s)" (car ls) (cadr ls)))
((putChar)
(format "(~a ~s ~a)" (car ls)
(cadr ls) (caddr ls)))
((branch)
(format "(~a ~a ~s ~a ~s)"
(first ls) (second ls)
(third ls) (fourth ls) (fifth ls)))
((jump)
(format "(~a ~a)" (car ls) (cadr ls)))
(else
(format "(~a)" (car ls))))))
(format "(msg ~s)"
(string-trim
(string-replace line "\\n" "~%"))))
acc)))))))
長い関数で「ゲ」とか思うかもしれないが、上で記述したscenario.txtの「仕様」(らしきもの)に従って一行毎に読み込んだモノの要素を粛々と置き換えていってるだけ、である。
文字列の置き換えで大活躍するのはSRFI-13のstring-replaceである。
ただし、SRFI-13全体を読み込んでしまうとRacketに同梱されている同名関数を全部シャドウイングしてしまう。
そして一行の冒頭が#が付く場合、二個目も#な場合、それ以外の場合、と基本3パターンに分けてformatで整形していくのだ。
また、オリジナルのscenario.txtでは、地の文が「セリフ」と解釈されてるが、S式版では文字列を引き連れたmsgコマンドに変更されている。
さて、translator関数にscenario.txtを食わせると、以下のようなscenario-rkt.txtを生成する。
(back "img8/chap8-back1.png")
(msg "【??】~%あ!~%リリーさーん!")
(msg "【リリー】~%だ、だれだ!?")
(msg "ドーン!")
(msg "【??】~%きゃー!ごめんなさい!")
(putChar "img8/chap8-chara1.png" C)
(msg "【??】~%こんにちは!授業初日お疲れさま〜!")
(msg "【リリー】~%お、おう……。(誰だこいつ?)")
(msg "【マコ】~%あ!はじめましてでしたよね!~%私、同じクラスの桜庭マコっていいます!~%よろしくね!")
(msg "【リリー】~%(こんなやついたっけ……思い出せない)おう、よろしく。")
(msg "【??】~%おーい!マコー!~%……~%あ、いたいた!")
(putChar "img8/chap8-chara3.png" L)
(msg "【??】~%もう、探したんだからね?~%えっと、この子は……あ!転校生のリリーちゃんね!")
(msg "【ハル】~%私は清水ハル!よろしくね!")
(msg "【リリー】~%(だめだ、全然思い出せん……)~%よろしくな。")
(putChar "img8/chap8-chara2.png" R)
(msg "【??】~%マコっち〜ノアおなかへったよ〜~%……~%あ!だれこの子!なにその本!かっこい〜!")
(msg "【リリー】~%(また増えた……って、お前こそ誰だ)~%私は竜崎リリーだ。きみは?")
(msg "【ノア】~%あ!思い出した〜転校生の子だ!白鳥ノアだよ〜!~%ね〜ね〜みんな、今日は天気がいいし遊びにいこうよ〜!")
(msg "【ハル】~%ちょっとノア、明日のテストはどうするつもり?前回も赤点だったじゃない!")
(msg "【ノア】~%ふっふっふ、このノア様がテスト勉強なんてすると思うか?")
(msg "【マコ】~%しないですよね……~%あ、私はちゃんと勉強してきたから大丈夫ですよ!")
(msg "【ノア】~%さすがマコっち!あっそぼ〜!")
(msg "【ハル】~%もう、仕方ない子ね……私はもう少し勉強したいから、先に帰るわ。~%リリーちゃん、また今度遊んでちょうだいね?")
(msg "【リリー】~%おう。またな。~%(それで、私は行くことになってるのか……?)")
(branch 1 "遊びに行く" asobu "y")
(branch 2 "帰る" kaeru "n")
(label asobu)
(msg "【ノア】~%やった〜!いこいこ!")
(msg "【ハル】~%もう、あんまりリリーちゃんを振り回しちゃだめよ?~%マコ、ノアをよろしくね。それじゃ、私は帰るわ。")
(msg "【リリー】~%おう、またな。(こいつはまともそうだな……)")
(msg "【ノア、マコ】~%ハル、またね!")
(jump common)
(label kaeru)
(msg "【ノア】~%え〜!?帰っちゃうの!?やだ!遊ぼうよ〜!")
(msg "【リリー】~%(断るのも面倒だし行ってみるか……)~%まあ、テストは大丈夫そうだし行くよ。")
(msg "【ハル】~%こら、ノア!リリーちゃんを困らせないの!")
(msg "【ノア】~%え〜!?テストが大丈夫ならいいじゃん!")
(msg "【リリー】~%まあ、私は大丈夫だよ。")
(msg "【ハル】~%そう?ならいいけど……~%じゃあ、私は帰るわ。あとの子守りはよろしくね、マコ。")
(msg "【ノア】~%ちょっとハルっち、子守りって何!?")
(msg "【リリー】~%(うるさいなこいつ)おう、またな。")
(msg "【マコ】~%ハルさんまたね〜。テスト勉強がんばってくださいね!")
(jump common)
(label common)
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【ノア】~%さて、どこに行こっか!")
(msg "【マコ】~%右に行くと神社があって、左に行くと駅がありますよ!")
(msg "【リリー】~%(どっちでもいいな)ノアとマコが好きな方でいいよ。")
(msg "【マコ】~%私は神社に行きたいです!")
(msg "【ノア】~%ノアは駅前で遊びたい〜!")
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【マコ】~%え〜!~%……それじゃあ、リリーさんが行きたい方にしましょう!")
(msg "【リリー】~%(本当にどっちでもいいんだが……)~%私は……")
(branch 1 "神社に行きたい" jinja "y")
(branch 2 "駅前に行きたい" ekimae "n")
(label jinja)
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【ノア】~%リリーが行きたいんだったら仕方ないな〜")
(msg "【マコ】~%ふふ、じゃあ行きましょう!")
(msg "……")
(back "img8/chap8-back3.png")
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【ノア】~%着いたよ〜!")
(msg "【マコ】~%やっぱり神社は落ち着きますね!")
(msg "【リリー】~%(それはわかるな)うん、たしかに")
(msg "【マコ】~%ふふ、リリーさんって見かけによらず優しいですよね。急に誘ったのに来てくれましたし")
(msg "【ノア】~%たしかに!ハルっちみたいに厳し〜くないよね!~%……ハルっちも来れたらよかったのにな〜")
(msg "【リリー】~%まあ、またみんなで来よう。")
(msg "【マコ】~%ですね!いいお友達が増えてうれしいです!")
(end)
(label ekimae)
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【ノア】~%やった〜!駅はこっちだよ!")
(msg "【マコ】~%ノアさん、ちょっとまってください〜!走ると危ないですよ!")
(msg "【リリー】~%(マコはノアの保護者みたいだな)")
(back "img8/chap8-back4.png")
(putChar "img8/chap8-chara1.png" C)
(putChar "img8/chap8-chara2.png" R)
(msg "【マコ】~%着きました!")
(msg "【ノア】~%ノア、ソフトクリーム屋さんに行きたいな〜!")
(msg "【マコ】~%あっ、いいですね!リリーさんはどうですか?")
(msg "【リリー】~%私も行きたいな。")
(msg "【ノア】~%やった〜!行こ行こ!")
(msg "【リリー】~%(なんか楽しいな……いい友達ができてよかった。~%このままこれからの学校生活も楽しめるといいな)")
(end)
もう一つ注意点を言うと、テキスト情報上での"\n"(改行文字)がテキスト操作に合わせて"\\n"と改変される事が起きる。Python原作でもそこを避けるようなハックが書かれている。
Racket版ではいっそ、Lispでの由緒正しい改行文字"~%"へと置き換えている。big-bangではLispらしくなく"~%"を受け付けず、"\n"にまた戻さないとならないが、その辺は大した手間でもないだろう(※18)。
さて、Racketにscenario-rkt.txtを読み込んでみよう。次のようなデータ(大域変数)を書く(※19)。
(define *scenario*
(with-input-from-file "img8/scenario-rkt.txt"
(lambda ()
(let loop ((s-expression (read)) (acc '()))
(if (eof-object? s-expression)
(reverse acc)
(loop (read) (cons s-expression acc)))))))
そうするとS式を見かけたread関数が一つづつS式を読み込み、*scenario*変数が持ってるaccリストにそのS式は一つづつツッコまれていく。
*scenario*変数を実行して見てみよう。
シナリオがS式のリストとして読み込まれてるのが分かるだろう。
これで調理スタート、である。
次回はまずは基本となるCLIベースでのインタプリタ(アドベンチャーゲームエンジン)作成から始めようと思う。
※1: 繰り返すが、個人的にはこの本は買ってもいないし読んでもいない。
※2: 殊更Racketのドキュメンテーションを貶してるわけではない。
っつーかScheme実装のドキュメンテーションの質の悪さはどれも似たりよったりであり、殆ど唯一の例外が川合史朗氏制作のGaucheだと言って良いだろう。
結果、Gaucheが日本で一番人気があるLispなのは当然だと言える。
※3: 「大学でゲームの作り方を教えるなんて」と眉を顰める層もいるかもしれないが、視野が非常に狭いと思ってる。
ある意味、「プログラミング教育に携わる人間はバカなのか?」って事なんだが、多分半分は正解でも半分は違うだろう。
言い換えると、単に「プログラミングをどうやって教えるのか」と言うノウハウがまだ無い、と言う事なのだ。
我々が何となく受けてる義務教育上の算数・数学だが、このカリキュラムは実は紀元前3世紀頃に作られたユークリッドの「原論」を元にしている。つまり、我々は2,000年以上に渡る数学教育の「ノウハウ」に則って算数・数学を勉強してたのだ(例外は「近代に登場した」関数と確率だけ、である)。
比するとコンピュータ・サイエンスはたった100年の歴史もない。従って確固たる教育法がまだ「分かってない」と言うのが本当のトコだろう。新参も新参、要するにひよっこなのである。
言い換えると、教育方法とは一朝一夕で成り立つようなモノではない、と言う事で、同時に歴史の重みがあるべきものなのだ。
(こんな状態なのに小学生から学ばせる、なんつーのは正直、狂気の沙汰としか思えない)
※4: 「魔法言語リリカル☆Lisp」のエンジンである。
※5: かつて、エロゲで唯一複雑な構造をしてて「ゲームとして面白い」と思ったのはPC-9801でリリースされた「YU-NO」と言うゲームしか無かった。これ「だけ」は一方方向「だけ」に進まず、事象同士が複雑にリンクしてる前提じゃないと作れないゲームであると思う。・・・いわば、技術的にはグラフィカルなアドベンチャーゲームにテキストアドベンチャーゲームの「仕組み」を導入してる殆ど唯一の例じゃなかろうか。
※6: あくまで「基本路線は」である。実際は構造化された汎用プログラミング言語のような処理もある程度可能となるように設計されている。
※7: ちなみに、評論における「構造主義」だと、時系列もクソも関係なく、こういう風に物語をぶった切って論評しても良いらしい。
※8: もっともグラフィックのないエロゲはクリープが無いコーヒーのようなモノだ、とお怒りの諸氏もいるたぁ思う。
※9: インタプリタの仕組みはこのように、与えられた情報を「どう評価するか」決める評価部(Evaluator)を作り、あとは読み込んだ(Read)情報を評価した結果を印字(Print)する、と言う動作を延々と繰り返す(Loop)するだけ、である。
この仕組みをRead-Eval-Print Loop、略してREPLと言う。
※10: 「バカバカしいプログラム」だと思うかもしれないが、これが基本だからしょーがない。
「だからノベルゲームやエロゲ作成なんざくだらない」とか思うかもしれないが、実のトコ言うと、この例は「最小限の何も出来ないインタプリタ」の実例でもあり、ここを否定すると「インタプリタを作成する」事もくだらなくなってしまう。・・・・・・両者とも技術的な基礎部分を共有してるから、である。
※11: もっとも、「もっと記述法が簡単な方が良い」と言うんで、2010年代辺りからは設定ファイル形式としてはJSONの方が人気がある。
※12: 正確に言うと、S式はLisp用語では
- アトムである事
- ないしはリストである事
である。
ただし、ここでは簡単の為、後者のリスト表現を指すこと、とする。
なお、アトムとは、シンボル、数値、文字列等、リスト以外でLispで扱う事が出来る基礎データ型の事を指す。
※13: 実際HTMLの記述ミスは山ほど多い・・・しかもWebブラウザは「HTMLは素人も記述するんで記述ミスも許す」実装になってる事が多く、HTML文法違反があっても見逃すように設計されている・・・・・・ありがたい、と思うかもしれないが、反面、Webスクレイピングをやろうとしたら、とんでもない手間になる事が多い。世の中、「HTML文法違反」を噛ましてるWebサイトの方が多いくらいで、自作のHTMLパーザやらPythonのビルトインライブラリを用いるとマトモにパージングが出来ないページばっかり引っかかる、と言うトンデモない状況に陥るのが日常茶飯事である。
結果、勢い付けてやり始めたはいいけど、30分もすると「やーめた!」となるのも珍しくないのだ。
全て、HTMLは「プログラムが構文解析する」には易しい形式だが、人間が手書きするには手間である、と言うエルゴノミクス的な設計上の欠陥に由来している。
それを考えると「括弧の帳尻を合わせるだけ」のS式の方が遥かにマシな形式なのだ。
※14: HTMLもS式も「パーズしやすい」と言う前提だと、相互に変換するのも思ったより簡単だ、と言う事が分かると思う。
特にLisp上だと、例示のようなS式型HTMLをすぐさま通常のHTMLに「変換する」関数なりマクロなりを作るのはたやすい。
と言う事は、(ハードウェア的な意味での)サーバーがあり、Lisp処理系がそこにインストールされていれば、原理的には、他の言語に比べるとCMS(コンテンツ・マネージメント・システム)を作るのは簡単だ、と言う事になる。Lispでプログラムを書けば即刻HTMLに変換可能だから、だ。
なお、ポール・グレアムのANSI Common Lispと言う本の第16章では、実際そういうアイディアでANSI Common LispでHTMLを扱うユーティリティを提案している(そしてそのテキスト上のコードが実際の、後のVerizon Small Business Essentialsの雛形になったらしい)。
※15: ある「表現」がデータなのかプログラムなのか、と言うのはレイヤーによって違う、と言うのが答えになる。
例えば貴方がPythonでJavaScript処理系を作ったとする。Python視点では読み込むべきJavaScriptファイルは単なる「データ」だが、貴方が作ったJavaScript処理系にとっては「プログラム」である。
要するにある「表現」が「データ」なのか「プログラム」なのかは相対的であり、どのレイヤの視点でどっちにあたるのか、を言わないとならない。
反面、Lispが特異なのは、Lispが扱うS式は「データ」でもあり「プログラム」でもある事だ・・・Lispにはデータとプログラムの境界線がない。だからLisp自身の構文を弄る「マクロ」等と言うおかしな機能を持つ事が出来る。
・・・つまり、貴方がPythonで作ったJavaScript処理系はPythonによって構文を弄れるが、PythonでPython自身の構文を弄る事は基本的に出来ないし、貴方が作ったJavaScript処理系でそのJavaScriptの構文処理を弄る事は出来ない。
反面、LispはプログラムもS式、データもS式なんでそのテの敷居が存在しない。だからLisp自体の文法を弄れる「マクロ」が成立するわけである。
なお、データ=プログラムである言語は、知ってる限り二つしかない。Lispと機械語である。機械語は単なる数字の羅列、つまりバイナリなんで、CPUにとってはある時はプログラムであり、ある時はデータになる。
Lispがデータ=プログラムの形式を持つのは、単純に、最初に発表された理論が「人間に読み書きしやすい機械語の代替案」として提案されたものだからだ。
※16: 加えて言うと、S式でシナリオを記述するとすれば、そこに含んだ「命令」は、極端な話、関数を設定しておけばLispの評価器、evalに通してそのまま実行させる事さえ可能である・・・・・・ただし、何らかの入力が絡む場合、この方式は危険だ、と言うのはLand of Lispを始め色んなLisp関連の書籍で指摘されている。どんなコマンドがユーザーから入力されるか分からんから、だ。
実際はGUIのソフトウェアだと入力制限がCLIのソフトウェアよりキツイケースが多いので、このテのグラフィカルアドベンチャーのゲームではそこまで神経質にならなくて良いと言えば良いのだが、今回はオーソドックスなスタイルに従って直接evalを用いる形式は避ける事、とする。
※17: 仮にLisp以外の言語でアドベンチャーゲームエンジンを作成するにせよ、シナリオ記述形式にS式を使うのは一考に価する、と思う。パーズ対象として考えてもXMLやJSONの類より明らかに実装が簡単だから、である。下手な自作言語を作るより良好な結果を得られるのではないか。
言い換えるとS式は単純で、かつフォーマットが決まりきってて、字句解析及び構文解析エンジンを作って扱う対象としても最良なのだ。
この「簡単さ」と言うのが、この世に「俺様Lisp」がクソの山程存在する理由である。
※18: 何度も言うが、Scheme系は長らくUNIXとベッタリな期間が長いため、ANSI Common Lispのような"~%"と言う改行文字はポピュラーではなく、拡張表現として"\n"を用いてるケースが多いようだ。また、元来、整形出力は仕様上Schemeには備わっていなく、あくまで改行は#\newlineと言う「文字」が担っていて、また出力関数としてはnewlineがあるのみ、である。
その辺が、意図としてLispマシン上のLisp OS作成を視野に入れたANSI Common Lispとミニマリズムを標榜してるSchemeの差でもある。
※19: 場合によってはRacketが特定の(ホーム)フォルダで起動して狙ったフォルダに収めたファイルを開けないかもしれない。
その場合は、
(current-directory (path->directory-path "狙ったフォルダのホームからの相対パス"))
と言うコードを付け加えれば良い。