見出し画像

Retro-gaming and so on

ババ抜きを作ろう その1

0. Preface

Read-Eval-Print Loopと聞くとどうしてもインタプリタを想像する人が多いと思う。
しかし、以前指摘した通り、スクリプト以外のソフトウェアは全て何かしらのRead-Eval-Print Loopを持っている。
コンピュータサイエンスだとインタプリタ作成を通じてREPLの存在を教えるが、「全てのソフトウェアで使える手法である」と言うのをあまり強調してないのではないだろうか。
一方、実用Common Lisp(PAIP)Land of Lispと言う書籍では殆どハッキリ

「全てのソフトウェアはREPLを通じて作られる」

と語ってる。他の☓☓言語入門、なんかのヘナチョコな書籍では語られない「ソフトウェアの作り方」にズバリ斬り込んでいるのだ。

関数elizaの構造は共通のパターンである。再度elizaのコードを示す。

(defun eliza()
 "Respond to user input using pattern matching rules."
 (loop 
  (print 'eliza >)
  (print (flatten (use-eliza-rules (read))))))

Lisp自身も含めて、多くの他のアプリケーションでこのパターンを使用している。Lispのトップレベルは次のように定義できる。

(defun lisp()
 (loop
  (print '>)
  (print (eval (read)))))

〜中略〜

プロンプトを省略すれば、完全なLispインタプリタを、わずか4つのシンボルで書ける。

(loop (print (eval (read))))

4つのシンボルと8つの丸括弧によるLispインタプリタの構成は冗談だと思うかもしれない。

〜中略〜

重要な点は、1行のコードをLisp処理系と見なせるかどうかではない。計算に関する共通のパターンを認識することだ。elizaとLispは両方とも、入力を読み込み、入力されたものをある方法で評価または変換し、結果を出力し、それからさらになる入力を読み込むために戻る。したがって、次のような共通パターンが引き出せる。

(defun プログラム()
 (loop
  (print プロンプト)
  (print 変換 (read))))

全てのソフトウェアがREPLとして直接実装されているわけではないが、まずは実行結果を見ると明らかにそう見えるのである。
そして「共通パターンが垣間見れる」以上、その「共通パターンに則って」プログラムを書くべきだ、と言う事だ。そして則らないで書けばたちまち「スパゲッティコード」になってしまうだろう。
以前書いたアドベンチャーゲームはインタプリタ処理系とグラフィカルアドベンチャーゲームエンジンの類似性を強調して話を展開したわけだが、別の例になるモノが何かねぇかな、とか考えていた。
そこで、今回はCLIでのババ抜きのプログラムを書いてみようと思う。あらゆるソフトウェアはREPLと言う「形式」に従った方が明快になる、と言う実例になるだろう。

1. ババ抜きとREPL

例によって星田さんが以前こういう事を書いていたのだ。

あと、実際のPythonで動いているゲームのプログラムも見てみようと思ってババ抜きプログラムを読んでたんだけど・・・今までのHTMLとかOPMLも変換のとかってのは上から流れてって、最後に出力して終わりってので分かるんだけど、ババ抜きプログラムだと役割もClassとして決めて(カードとかプレイヤーは分かるんだけど、ディーラーとかメイン動作もClassとして作ってる)、全部の作業をそれぞれの役が引き受ける感じなんですよねぇ・・Classってモンスターとかトランプのカードとか一複数の属性を持ったデータとかを効率よく管理するための鋳型ってイメージがあったので、メイン部分までClassのかぁ~とか

混乱するのも良く分かる(笑)。
敢えて言っておくと、そのページの筆者は、後にGUI版に改良しよう、と目論んでいたみたいだ。
そしてそのテのグラフィカルインターフェースを作成するツールキットの性質上、オブジェクト指向で書かなければならない、と言う制約がしばしば生じるわけだな。
言い換えると、「プログラムを書く上での」本質的なスタイルの要請ではない、と言う事になる。
そしてオブジェクト指向で書かれたプログラムは、正直言うと、ポール・グレアムが言った通り「構造化スパゲティコード」としか言いようがない。オブジェクト指向で書く事にメリットが全くない、とは言わないが、非常に読みづらい。書くのも大変だと思う(※1)。
例えば、そのページには図解で「ババ抜き」の動作モデルが表示されている。

1:

2:



3:



パッと見簡単だと思う?
僕のアタマの中ではこれでもう充分複雑だと思うわけだ。スパゲティの一歩手前だ。あっちゃこっちゃ指令が「飛んでて」これをプログラムするのは大変じゃないか。
一方、Read-Eval-Printループ、と言うモデルだと単にこれだけ、である。


初期化のプロセスを除くとあとはRead部、Eval部、Print部の間でグルグル回ってるだけ、である。画像も一枚で済むし(笑)。
お分かりだろうか。Read-Eval-Print Loopと言う「モデル」の方が見通しはいいのだ。
繰り返すが、件のページは「後にGUIに直そう」と言う前提でオブジェクト指向を用いてクラスで設計している。
しかしながら、そうでもなければ、「オブジェクト指向」と言う形式は必ずしも設計の見通しが良い、と言うわけじゃあない。
むしろ、REPLなんかのモデルを使えば簡単に記述できる事がいたずらに複雑になる可能性さえある、って事だ。

2. 言語インタプリタのREPLとゲームのREPLの違い

ただし、ゲームをREPLで作る際に一つ気をつけておかなければならない事がある。
言語インタプリタの場合、上のLispの例で見るように、evalの評価結果をそのままprintに手渡せば済むケースが多い。何故ならそれ以上の出力は特に必要がないから、だ。従って、一般的に言うと、言語インタプリタの場合、evalは肥大化するが、printreadは言語組み込みのビルトイン関数をそのまま使っても問題がないケースが多いわけだ。
一方ゲームの場合、ちと様相が違う。なんせ圧倒的に出力情報が「凝ってる」場合が多いから、である。従って、単純に言語が提供してるビルトインの出力関数の仕様だけだと「間に合わない」ケースが散見するのである。
言い換えると、REPLを利用してゲームを作る場合、

  • 何を本当に出力とするのかしっかりと設計しなければならない。

と言う事である(と言う事は、evalが一体「何を」手渡すかもキチンと考えないとならない)。ここをおざなりにすると、とんでもない迷宮に迷い込む事になるだろう。
そして、場合によっては入力関数も自作せねばならない場合も考えられるが、大事な原則を守ろう。それは

  • 自作の入力関数や出力関数は情報を貰っても決してその上でその情報を改変してはならない。
と言う事である。
REPLを用いたのにスパゲティ化する最大の原因はこれだ。入力関数や出力関数に「計算」の役目も兼任させた時、REPLモデルは崩壊する。計算はあくまでevalに任せてその他のパーツには「余計な事は一切させない」と言うのがキモなのだ(※2)。

なお、今回はまたもや、このページのPythonプログラムの出力結果に完全に合わせた。僕自身が「出力はこうして・・・・・・」って設計したワケではない。いわばSame Result, Different Systemなわけだが。
逆にそのために、出力関数弄りでやたら時間がかかってしまった(苦笑・※3)。
皆さんがREPLでゲームを自作する場合は出力情報をどうやり取りするのか、は自分でキチンと考えて決めて貰いたいものだ(人任せだと僕みたいに大変な思いをする事になる・笑)。

なお、今回のババ抜きの場合、evalにあたる部分は、前のアドベンチャーゲームみたいに言語インタプリタ的な複雑さは持ってない。
基本的には、むしろ簡単なスクリプト程度のモノとなる。

では始めよう。

3. evalの為のユーティリティを作る

もう一回各パーツの役割を押さえよう。

  1. 入力を司るread部。
  2. ゲームの処理を司るeval部。今回のババ抜きの場合、カードを引く、そしてペアとなったカードを捨てる、と言う二つが基本的役割。
  3. 出力を司るprint部。
evalは上で見たように、基本的にはたった二つの役割しか持ってない。
持ってないが、先にも書いたが出力絡みの部分がちと複雑で、「出力させるデータ」も合わせて返り値としなければならない、と言うのが工夫が必要なところである。
具体的には、「何のカードを引こうが」「どんなカードを捨てようが」全部隠蔽して書く方がラクなのである。キチンと計算処理さえしてしまえばゲームは問題なく進行する。
進行はするが、Python原作がかなり複雑な出力結果を求めていた為、環境設定と合わせてあーでもねぇ、こーでもねぇ、と弄りまくってたわけだ。
取り敢えずその辺の結果はあとで見てもらうとして、まずはカードそのものを作ったり、カードを引いたりペアを捨てたり、というeval本体に組み込む為の関数辺りから作っていこう。
まず今回は、ライブラリとしてはSRFI-1circular-listlset-differenceと言う二つの関数を利用する。

(require (only-in srfi/1
       circular-list
       lset-difference))

ここで導入した二つのライブラリ関数については後述する。
そしてトランプを作るわけだが、取り敢えずRacketの構造体、structを利用しよう。

;;; トランプの構造体

(struct Card (suit number) #:transparent)

numberが何なのか、と言う説明は必要ないだろう。suitと言うのはスペード、ダイヤ、ハート、クラブ等のマークの事だ。

次に「カードを引く」関数を作る。
Python原作と違い、オブジェクト指向じゃないし、「カードを加える」「カードを手放す」等と言うまだるっこしい過程を省き、次のような関数を一気に作る。

;;; カードを引く

(define (drawCard player1 player2 pos)
 (
let-values (((head tail) (split-at player2 pos)))
  (let ((card (car tail)))
   (
values card (cons card player1) (append head (cdr tail))))))

まず、player1player2もPython版のようなクラスで設計されたオブジェクトではなく、単なる「カードで構成されたリスト」を想定している。
また、仮引数のposと言うのは、player2と言うリストの「何番目」かを指定するインデックスだ。
単純な発想としては、player2のリストのpos番目の要素を抜いてplayer1のリストにconsする、と言う事だ。これがplayer1player2から「カードを引いた」事を表現してるし、その時点でplayer2のリストから該当する要素が一つ減らなければならない。
pos番目の要素を「抜く」のに利用するのが多値関数split-atである。この関数は与えられたリストをpos番目の要素の直前で二つのリストへと分割する。

> (split-at '(a b c d e f g h) 3)
'(a b c)
'(d e f g h)
>

上の例を見たら分かるが、この関数は「返り値が二つ」ある。フツーの関数は返り値は一つだけだが、二つ以上の返り値を持つ関数を特に「多値関数」と呼ぶ。
そして上の例では「リスト'(a b c d e f g h)を3つ目の要素の直前」で分割してるわけだ(要素番号は0から数えるから混乱しない事)。
多値関数の返り値を束縛するのはletじゃ不可能なので、代わりに多値関数用の束縛機構であるlet-valuesを用いる。これによりheadtailと名付けた変数名で返り値をそれぞれ受け取る事となる。
目指すcardtailcarなんで、それを「引いたカード」とする。あとはそのcardplayer1consして、player2(append head (cdr tail))にすればいいだけ、なのだが・・・・・・。
注意事項。上のコードを見れば分かるが、取り敢えず返り値を

(values card (cons card player1) (append head (cdr tail)))

としている。valuesはそれこそ「多値を返す為」の機構である。
つまり、関数drawCardは多値関数として設計されているのだ。
そして平たく言うとこの関数の返り値は「cardplayer1player2」と言う3つなのだ。
まずcardが返る理由は、要するに出力がそれを要請してるからだ。それが理由でplayer1consされたら即退場、たぁ行かなかったわけだ。
それより、player1player2が「同時に返らなければならない」方が切実だろう。これらは引数として受け取ったplayer1player2とは既に違う。player1はカードが1枚増えてるしplayer2はカードが1枚減ってるのだ。
これら二つの情報を同時に伝えないとゲームにならないのだ。
人によってはいきなり多値関数、なんてモノが出てきてビビるかもしれない。しかし、ビビる必要はない。こんな機能があるのは便利だから、なのだ。
ポール・グレアムは次のように書いている

他のプログラミング言語では,副作用を使う理由で最も大きいものは, 多値を返す関数が必要になることだ. 関数が1個の値しか返せなければ,他の値はパラメータに変更を加えることで「返す」しかない. 幸運なことに,Common Lispではその必要はない. どの関数も多値を返せるからだ. 

逆に言うと、もしここで多値が返せなかったら、例えばplayer2を大域変数としてなんかに代入するか、あるいはplayer2を無理矢理「破壊的変更」してデータを書き換えないとならない(このテの事がC言語プログラミングなんかでは良く起こる)。
しかし、多値関数があれば、関数型プログラミングを逸脱せず、「新しく生成したplayer1player2を」返すだけで済む。つまり、データをどこかで無理矢理破壊的に変更してデバッグする時に混乱する、なんつー事を味合わなくて済むのである。
多値関数は決して怖いモノではない。むしろバンバン使って構わないモノだ。
なお、Pythonでもタプルを利用した多値関数的な挙動を作るのは可能なんで、ぶっちゃけた話、addCardとかreleaseCardなんて言うメソッドを作る必要はないのである。

次はカードを捨てる関数を書く。

;;; カードを捨てる

(define (discardPair player)
 (let ((p (map car
       (
filter (lambda (x)
           (odd? (length x)))
          (
group-by Card-number player)))))
  (values (
lset-difference equal? player p) p)))

Python原作と随分と違うように感じるだろう。よって「何が起こってるのか」説明が必要かもしれない。
この関数がやってる事は基本的には次のようなモノだ。

  1. 手持ちのカードを数字によってグルーピングする=>リストのリストが生成される。
  2. グルーピングされた要素(リスト)のうち、要素数が偶数になってるものを除外する。
  3. 結果、手持ちのカードは要素数が奇数になってるリストが残るが、それらの先頭だけを採用する。
この3つの過程により、「ペアを除外する」効果が生まれるわけだ。
特に重要なのは2番目で、「グルーピングされた要素リストの長さが偶数」の場合、これはカードのsuit数(スペード、ダイヤ、ハート、クラブの4つしかない)の要請により、2のケースと4のケースが考えられるが、いずれにせよ、「全部捨てて構わない」と言う事になる。
また、「要素リストの長さが奇数」の場合、同様にカードのsuit数の要請で、1のケースと3のケースが考えられるが、どっちにしてもこの時点で「要素リストの先頭」しか要らないので、同じ数字が3つ、のケースのうち2枚は排除出来る。
こうやって手持ちのカードから「重複要素」を取り除くのである。関数型プログラミング的だろ(笑)?
1の過程で用いるRacket組み込みの便利関数がgroup-byだ。書式は以下の通り。

(group-by キー リスト)

キーで「何が同等なのか」を指定する。このプログラムの場合、先に設定したトランプの構造体により、Card-numberを指定する。
後でトランプを一気に作るが、取り敢えず、次のような例を想定しよう。なお、Cardの要素は全部文字列として、スペードをS、ダイヤをD、ハートはH、クラブをCとする。また、ジョーカーを(Card "J" "0")とする。

> (group-by Card-number `(,(Card "S" "2") ,(Card "S" "3") ,(Card "D" "2") ,(Card "D" "3") ,(Card "S" "Q") ,(Card "H" "3") ,(Card "S" "6") ,(Card "S" "J") ,(Card "S" "8") ,(Card "C" "3") ,(Card "S" "K") ,(Card "H" "2") ,(Card "J" "0") ,(Card "S" "A")))
(list ;; ここから返り値
 (list (Card "S" "2") (Card "D" "2") (Card "H" "2"))
 (list
  (Card "S" "3")
  (Card "D" "3")
  (Card "H" "3")
  (Card "C" "3"))
 (list (Card "S" "Q"))
 (list (Card "S" "6"))
 (list (Card "S" "J"))
 (list (Card "S" "8"))
 (list (Card "S" "K"))
 (list (Card "J" "0"))
 (list (Card "S" "A")))
>

カードの「数字」に従ってグルーピングされてる事に気づくだろう。
そしてこの「結果」を要素の「長さ」に従ってフィルタリングする。長さが奇数の場合は通して、そうじゃない場合は捨て去るわけだ。

> (filter (lambda (x) (odd? (length x))) (list
 (list (Card "S" "2") (Card "D" "2") (Card "H" "2"))
 (list
  (Card "S" "3")
  (Card "D" "3")
  (Card "H" "3")
  (Card "C" "3"))
 (list (Card "S" "Q"))
 (list (Card "S" "6"))
 (list (Card "S" "J"))
 (list (Card "S" "8"))
 (list (Card "S" "K"))
 (list (Card "J" "0"))
 (list (Card "S" "A"))))
 (list ;; ここからフィルタリング結果
  (list (Card "S" "2") (Card "D" "2") (Card "H" "2"))
  (list (Card "S" "Q"))
  (list (Card "S" "6"))
  (list (Card "S" "J"))
  (list (Card "S" "8"))
  (list (Card "S" "K"))
  (list (Card "J" "0"))
  (list (Card "S" "A")))
>

「3の数値を持つトランプ」は4枚あったので、それが全部消え去ってる。
残りは、各要素リストの先頭だけ返せば良い。

> (map car (list
(list (Card "S" "2") (Card "D" "2") (Card "H" "2"))
(list (Card "S" "Q"))
(list (Card "S" "6"))
(list (Card "S" "J"))
(list (Card "S" "8"))
(list (Card "S" "K"))
(list (Card "J" "0"))
(list (Card "S" "A"))))
(list ;; ここから結果
 (Card "S" "2")
 (Card "S" "Q")
 (Card "S" "6")
 (Card "S" "J")
 (Card "S" "8")
 (Card "S" "K")
 (Card "J" "0")
 (Card "S" "A"))
>

このプロセスで手持ちのカードからペアは完全に除去されるわけだ。
そして関数lset-differenceは集合演算の為の関数で、二つの集合の「差違」を返す。
つまり、元々あったカードの「集合」と現時点、上に見たようなペアを捨て終わった時点での「集合」の差違は、当然「捨てられたカード」になるわけだ。
そしてその「捨てられたカード」も出力に必要になるので、またもやvaluesを使って「捨てられたカード」と「現時点でのプレイヤーの手」の両方を返すようにしてる。

次に、ゲームのプレイヤーである「人」以外がカードを引く関数を作ろう。
とは言っても引く対象のリストからランダムに選べば良いだけ、だし、結局、整数乱数であるrandomをリネームすれば済むだけ、だ。

;;; num枚のカードから引くカードを決める
(define selectCard random)

こういうリネーミングは一見無駄に見えるだろう。
しかし、「書いたコードを明解にする」と言う原則だと、「そのプログラムに合った名前に付け替える」と言うのは結構バカには出来ない作業なのだ。
ちなみに、selectCard関数は、evalが使うのではなく、実際は入力制御に用いる(これを使う時は、プレイヤーがコンピュータで、入力を人から「奪う」必要があるため、である)。

4. 初期化に必要なユーティリティを作る

evalで使うユーティリティ作りは一旦終了する。
そしてevalに進む前、初期化に必要なカード周りに関する関数を作ろう。

まずはカードを配る関数、dealCardsだが、Python原作と違い、かなり大まかなハックを施している。

;;; カードを配る

(define (dealCards deck n)
 (let ((i (quotient (length deck) n)))
  (let loop ((deck deck) (acc '()))
   (cond ((null? deck) acc)
      ((< (length deck) i)
      (let ((j (length deck)))
       (loop '() (append (map cons deck (
take acc j))
             (
drop acc j)))))
      (else
       (loop (
drop deck i) (cons (take deck i) acc)))))))

お分かりだろうか?カードを一枚づつ配っていない(笑)。
ここではカードを一枚づつ配るのではなく、カードの塊を単に人数で分割して纏めて渡してるのである。
これは、こだわる人はこだわるんだろうが、言わせてもらえば、乱数でカードをシャッフルする以上、どう渡そうが結果同じなのだ(そして同じじゃないといけない)。
言い換えると、トランプで人対人が対決する場合、イカサマが絡む場合もあるので、「公平性を演出する為」、一枚一枚カードをプレイヤー陣に順番に配る必要性が生じるのだ。
しかしながらコンピュータ上では・・・プログラムに悪意を混ぜない限り、「人の目に見えるような」公平性を演出する必要性が全くない、のである。
どっちにせよ演算は(人間の観点では)一瞬で終わるので、公平性の演出もクソもねぇのだ(※4)。
だとすれば、プログラミングする側では「ラクなプログラムの方が良い」ってこった。単に生成された乱数の結果を信じよう。
この関数は基本的には

  1. カードのリスト(deck: カードデッキ)をゲーム参加人数で分割する(そして余りが出る)。
  2. プレイヤー一人一人に分割されたカードの塊を渡す(プログラム上はtakedropを使いながらアキュムレータ(acc)にtakeされたリストを順次consしていく)。
  3. 残ったカードの余りの数を調べて、その数に一致するだけのプレイヤー数をaccから選び、余りを一枚づつconsしていく。終了したらプレイヤー達をaccに戻す。
  4. deckが空になったらカード配りは終了。
例えば、ジョーカーを含めるとトランプは53枚ある。
ババ抜きのプレイヤー数が4名とすると、この時点で各プレイヤーに手渡す枚数は13枚、と確定し、余るカードの数は1枚だ。この時点でプレイヤーそれぞれには13枚の手札があれば良い、って事だ。
余ったカードの1枚はプレイヤー陣から1名選び(このケースだと、プログラム上はリストの先頭のヤツになる)、そいつに1枚手渡せば「ディーリングは終了」と言う事だ。プログラム上、わざわざ順番に一枚一枚配る必要は全くない。
ここで知ってもらいたいモノは、「現実がこうだから」と言うんで必ずしもそれにプログラム上従わなくても良い、ってこった。結果が同じならラクな方を選ぼう。

次はカードをシャッフルする関数を書く。
ぶっちゃけると、Racket組み込みの関数、shuffleをそのまま使っても良いのだが、人間心理としては「良く混ぜたい」と思うのも確かだろう。
数学的な考察ではトランプを「一回混ぜよう」と「n回混ぜよう」と結果は変わらないのだが、一応ここでは「n回混ぜる」ように関数を設計している。

;;; カードをシャッフルする

(define (shuffle-list lst n)
 (
foldl (lambda (x y)
    (
shuffle y)) lst (range n)))

もちろん、名前付きletでルーピングしても良いのだが、高階関数foldlで記述した方が簡単である。
初期値はトランプのリストであるlstで、これにn回分だけshuffleを適用する。
Pythonのrangeと同じ作用の関数で作られてるリストは単純にカウンタとして使われてるだけでshuffleには影響しない。しかし、そのリストが消費され終わった時、乱数による計算が終了する。

さて、あとはトランプを「作る」だけだが、それも一旦脇に置いておいて、次は出力用のメッセージを作る関数を組み立てていこう。

5. メッセージ分離方式

まずは出力する為のメッセージをまとめた連想リスト(※5)を作っておく。

(define *messages*
 '((inputSelectCard . "何枚目のカードを引きますか? (0 〜 ~a)\n")
  (showNumCard . "プレイヤー~aのカード: ~a枚\n")
  (showCards . "プレイヤー~aのカード: ~a\n")
  (showDiscardCards . "プレイヤー~aが捨てたカード: ~a\n")
  (showDrawCard . "プレイヤー~a -> プレイヤー~a : ~a\n")
  (showResult . "プレイヤー~aの負けです...")
  (getCardStr . "[~a:~a]")))

「どういうメッセージを表示するのか」と言うのはメッセージを出力する関数に直接埋め込む事を避けた方が良い。
と言うのも、例えば、このババ抜きプログラムを英訳したい、とする。そういう場合、出力関数がアッチコッチにあって、なおかつそれに直接メッセージが埋め込まれてる場合、一々探し回って修正していくのは手間になる。
昨今のワールドワイドで使われるプログラムは、メッセージはメッセージで分離しておいて、ここではやってないが、例えば別のテキストファイルに纏めてる、と言う形式が多い。いずれにせよ「メッセージの内容」はプログラム本体から切り離しておいた方が後々都合が良い、と言う事だ。
これを「メッセージ分離方式」と呼ぶ(※6)。

さて、ここからはいくつかの「文字列を整形する」関数を書いていく。あくまで「文字列を整形する」だけで出力はしない。言い換えると後に作るprint関数で使う関数群を作っていくわけだ。
まずは手持ちのカードを表示する文字列を整形する関数、showCardsから作っていこう。

;;; 手持ちのカードを表示する文字列

(define (showCards players)
 (apply string-append
   (map (lambda (x y)
     (if (zero? x)
      (format
       (cdr (assq 'showCards *messages*))
       x (getCardsStr y))
      (format
       (cdr (assq 'showNumCard *messages*))
       x (length y))))
    (range (length players)) players)))

いくつか注意事項がある。
まず仮引数のplayersだが、これはリストのリストを想定している。トランプで構成されたリストを複数持ったリストだ。
そしてその先頭は実際プレイする人を想定している。それ以外はコンピュータだ。
従って、出力内容を「0番目とそれ以外」で分けないとならない。
そこで、mapで消費されるリストを二種類にしている(mapは可変長引数を取れる事を忘れないように!)。1つ目は「playersリストの何番目」を明示するリストでrangelengthを使って番号を生成している。
それらにより、map内では、「番号が0の場合、カードの中身を表示し、それ以外の場合、持ってるカードの枚数だけ表示する」と言う動作を振り分けるわけだ。当然、コンピュータの手が丸見えだったらゲームにならないからね。
もう一つの注意点は

(cdr (assq 'showCards *messages*))

の部分だ。「あれ?showCards関数と同名なんだけど再帰しないの?」と言う辺り。しかし再帰は起きない。
ある意味危険に見える、と言えば危険に見えるが、実は構わない。何故ならクオートされてるから、である。クオートされてるシンボルはそのまま、連想リスト*messages*のキーとして働き、事実上関数showCardsとは別物として認識されてる。
気になる人は関数名か連想リスト*messages*のキーの名前を変えても良いが、Lisp的にはこのテの「同名」はあまり気にしない(※7)。むしろコードの明解性の方を重要視する。言い換えれば、単純には、関数showCards*messages*からキー'showCardsに対応する値を引っ張ってくる関数なのである。
あとは、mapが作り出した文字列のリストをstring-appendで連結するだけ、である。
なお、getCardsStr関数はまだ作成してないが、これは後述しよう。

続く。

※1: 今、往年のBASICでコードを書け、って言われるとキツい、って思うだろうが、実は感覚的に言うとオブジェクト指向で書くコードは往年のBASICで書くような「大域変数使いまくりのプログラム」と良く似てると思う。
BASICはプログラムないしはファイル毎に大域変数を利用してコードを書くが、一方、事実上、オブジェクト指向は「クラス毎に」そういう状況を作り出してるだけ、である。
Pythonなんかでのself塗れの変数はそのクラス内では「どこからでも参照できる」。言い換えると、それらはクラス内での大域変数なのだ。
結果、オブジェクト指向が人気がある、と言うのは、BASIC的な思考でプログラムを書いてる人が事実上多い、と言う事に他ならない。

※2: GUIのREPLと言って良いMVCでデザインに失敗するケースは、同様に、Read部にあたるController及びPrint部にあたるViewで「余計な計算」をさせるから、のように思われる。ControllerもViewも原則「余計な事」をさせてはダメで、計算にまつわる部分は全てEval部にあたるModelに行わせないとならない。

※3: 実際、黎明期のBASICで書かれたミニコン/パソコンゲーム等は、goto塗れの割にはやたら出力に凝ってたゲームが多かったモノだ。と言うよりまさしく「出力こそ華だ」と言わんばかりで、確かにゲームと言うジャンルはそれが命だろう。
一方、そういった当時の人気ゲームはUNIXにも移植されたが、特にBSDに移植されたゲーム類は出力が簡略化されていたりした。と言うのもBASICはgoto塗れでもBSDを利用してた連中はREPLに忠実にプログラムを移植してたのだろう。よってあまりに凝ってて移植が面倒臭いようなゲームは敢えて出力を簡略化してたのである。

※4: 良くある例としては、例えばパチンコでの「確率変動」のプログラムがある。
如何にも「今確率変動が起こってますよ〜」と言うカンジでアニメなんかで演出されているが、実際のプログラム上では、既に確率変動が「成功」してるか「失敗」してるか、ってのは一瞬で決まっている。
要するにアニメでの演出は無駄なのだが、とは言っても演出は演出であり、プログラムの中身と演出が「一致してる」とは限らないのだ(実際は結果が「決まった」後、その結果に従って演出アニメが「選ばれる」と言った方が良い)。
同様な例として「おみくじプログラム」なんかもあるが、おみくじの箱を「振ってる」演出はするが、裏では既に乱数であっと言う間に結果は「決定」されているのだ。
繰り返すが「演出」と「プログラムによる演算」が完全に同期してる、とは限らないのだ。

※5: もちろんハッシュテーブルを使用しても良い。

※6: ロバート・ウッドヘッドWizardryは日本でスゴい人気になったんだけど、日本のパソコンで動いていたWizardryのソースコードは、アメリカのPC向けのオリジナルのソースコードと殆ど同じなんだよ。Wizardryはメッセージ分離方式で書かれたプログラムだったんで、そこを翻訳するだけで殆ど充分だったんだ。あとは、ハードの違いを少々修正するだけ、で良かったね。」

※7: このテの作法はむしろANSI Common Lispのモノで、名前空間が複数あるANSI Common Lispではデータ内容の違いに合わせて同名のシンボルを使いまくったりする。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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