プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(27)

気持ちのいい日曜日ですね!明日が休みってのが気持ちよさに拍車をかけます。
さあ今日は「Land of Lisp」やります。
構造体やジェネリック関数を用いたゲームを作っていくはずです。

オークバトル

プレイヤーは12体のモンスターに囲まれ、命を賭けた戦いに挑む騎士です。
戦いを慎重に進めなければ、全員を倒す前に数で圧倒されてしまうかもしれません。
defmethodとdefstructを使って、モンスター共に一泡吹かせてやろう!というゲームです。

プレイヤーとモンスターのグローバル変数

プレイヤーの体力と素早さと力をグローバル変数に格納します。

CL-USER> (defparameter *player-health* nil)
*PLAYER-HEALTH*
CL-USER> (defparameter *player-agility* nil)
*PLAYER-AGILITY*
CL-USER> (defparameter *player-strength* nil)
*PLAYER-STRENGTH*
CL-USER>

モンスターについての情報を格納する変数、モンスターを作成する関数を格納する変数、
モンスターの最大数を格納する変数も用意しておきます。

CL-USER> (defparameter *mansters* nil)
*MANSTERS*
CL-USER> (defparameter *monster-builders* nil)
*MONSTER-BUILDERS*
CL-USER> (defparameter *monster-num* 12)
*MONSTER-NUM*
CL-USER>

メイン関数

最初にゲームシステム全体を統括する関数を作っておくようです。

CL-USER> (defun orc-battle ()
           (init-monsters) ;モンスターを初期化する
           (init-player) ;プレイヤーについても初期化
           (game-loop)
           (when (player-dead)
             (princ "You have been killed. Game over."))
           (when (monsters-dead)
             (princ "Congratulations! You have vanquished all of your foes.")))
; compilation unit finished
;   Undefined functions:
;     GAME-LOOP INIT-MONSTERS INIT-PLAYER MONSTERS-DEAD PLAYER-DEAD
;   caught 5 STYLE-WARNING conditions
ORC-BATTLE
CL-USER>

大量に「その関数はまだ定義してないよ!」と警告が出たので省略しました。

さ、上の関数の肝の部分である、geme-loopを書きます。

CL-USER> (defun game-loop ()
           (unless (or (player-dead) (monsters-dead))
             (show-player)
             (dotimes (k (1+ (truncate (/ (max 0 *player-agility*) 15))))
               (unless (monsters-dead)
                 (show-monsters)
                 (player-attack)))
             (fresh-line)
             (map 'list
                  (lambda (m)
                    (or (monster-dead m) (monster-attack m)))
                  *monsters*)
             (game-loop)))
; compilation unit finished
;   Undefined functions:
;     MONSTER-ATTACK MONSTER-DEAD MONSTERS-DEAD PLAYER-ATTACK PLAYER-DEAD SHOW-MONSTERS SHOW-PLAYER
;   Undefined variable:
;     *MONSTERS*
;   caught 1 WARNING condition
;   caught 7 STYLE-WARNING conditions
GAME-LOOP
CL-USER>

またまた関数定義してないから怒られまし…?*monsters*は定義したはず…。
やべっ綴り間違えてたお恥ずかしい。修正修正。

CL-USER> (defparameter *monsters* nil)
*MONSTERS*
CL-USER>

さて、上の関数はまだ未定義の関数を呼び出している部分が多いので、今理解すべき部分があまりなさそう。
dotimesぐらいでしょうか。
dotimesは、変数名と数値を受け取り、本体にあるコードを数値回分繰り返すそうです。

CL-USER> (dotimes (i 10)
           (prin1 i)
           (princ ".spam! "))
0.spam! 1.spam! 2.spam! 3.spam! 4.spam! 5.spam! 6.spam! 7.spam! 8.spam! 9.spam! 
NIL
CL-USER>

プレイヤーとモンスターの双方がまだ生きていれば、プレイヤーに関する処理をして、
それが終わったらモンスターの処理をして再帰する…それしかまだわからない感じ。

プレイヤーに関する関数

まずはプレイヤーを管理する関数を書いていきます。

CL-USER> (defun init-player ()
           (setf *player-health* 30)
           (setf *player-agility* 30)
           (setf *player-strength* 30))
INIT-PLAYER
CL-USER> (defun player-dead ()
           (<= *player-health* 0))
PLAYER-DEAD
CL-USER> (defun show-player ()
           (fresh-line)
           (princ "You are a valiant knight with a health of ")
           (princ *player-health*)
           (princ ", an agility of ")
           (princ *player-agility*)
           (princ ", and a strength of ")
           (princ *player-strength*))
SHOW-PLAYER
CL-USER>

ゲーム開始時にプレイヤーのステータスを初期化する関数、
プレイヤーが死んでいるか判定する関数、ゲーム開始時にプレイヤーのステータスを表示する関数。
ここまでは問題ないです。次の攻撃を処理する関数は少し大変そう。

CL-USER> (defun player-attack ()
           (fresh-line)
           (princ "Attack style: [s]tab [d]ouble swing [r]oundhouse:")
           (case (read)
             (s (monster-hit (pick-monster)
                             (+ 2 (randval (ash *player-strength* -1)))))
             (d (let ((x (randval (truncate (/ *player-strength* 6)))))
                  (princ "Your double swing has a strength of ")
                  (princ x)
                  (fresh-line)
                  (monster-hit (pick-monster) x)
                  (unless (monsters-dead)
                    (monster-hit (pick-monster) x))))
             (otherwise (dotimes (x (1+ (randval (truncate (/ *player-strength* 3)))))
                          (unless (monsters-dead)
                            (monster-hit (random-monster) 1))))))
; 
; compilation unit finished
;   Undefined functions:
;     MONSTER-DEAD MONSTER-HIT MONSTERS-DEAD PICK-MONSTER RANDOM-MONSTER RANDVAL
;   caught 6 STYLE-WARNING conditions
PLAYER-ATTACK
CL-USER>

攻撃が3種類用意されていますね。
stabを選ぶと、monster-hitにpick-monsterの結果と、*player-strength*を1ビット右にずらして
半分にした値をranvalに渡してその結果を+2した値を渡します。日本語めちゃくちゃ。
randvalの定義は下記のようになります。

CL-USER> (defun randval (n)
           (1+ (random (max 1 n))))
RANDVAL
CL-USER>

これで最低を1としてランダムな値を得られるわけですね。
*player-strength*は30なので、最低3、最高17のダメージがstabです。

double swingは、2匹の敵にダメージを与える技です。
*player-strength*を6で割り、truncateで丸めて、randvalです。
最低1、最高5のダメージですかね。攻撃前にダメージの値を見せてくれます。
1匹目にダメージを与えて、敵が全滅してなければ次の敵にダメージを与えます。

roundhouseは*player-strength*を3で割った値をrandvalに与え、
その数を1+した回数分ランダムにモンスターを攻撃するようです。ダメージは1固定ですね。

さて、攻撃するために必要なrandom-monsterと、pick-monster関数を作っていきます。

CL-USER> (defun random-monster ()
           (let ((m (aref *monsters* (random (length *monsters*)))))
             (if (monster-dead m)
                 (random-monster)
                 m)))
; 
; compilation unit finished
;   Undefined function:
;     MONSTER-DEAD
;   caught 1 STYLE-WARNING condition
RANDOM-MONSTER
CL-USER> (defun pick-monster ()
           (fresh-line)
           (princ "Monster #:")
           (let ((x (read)))
             (if (not (and (integerp x) (>= x 1) (<= x *monster-num*)))
                 (progn (princ "That is not a valid monster number.")
                        (pick-monster))
                 (let ((m (aref *monsters* (1- x))))
                   (if (monster-dead m)
                       (progn (princ "That monster is already dead.")
                              (pick-monster))
                       m)))))
; 
; compilation unit finished
;   Undefined function:
;     MONSTER-DEAD
;   caught 1 STYLE-WARNING condition
PICK-MONSTER
CL-USER>

random-monsterはモンスターが格納された配列からランダムに要素を取り出し、
そのモンスターが既に死んでいたら再帰します。
pick-monsterは、プレイヤーに攻撃するモンスターを選ばせる関数ですね。
存在しないモンスターを選ばれたり、既に死んでいるモンスターが選ばれたら再帰します。

モンスターを管理する関数

まずはinit-monsetersです。

CL-USER> (defun init-monsters ()
           (setf *monsters*
                 (map 'vector      ;関数の結果を配列として返す
                      (lambda (x)
                        (funcall (nth (random (length *monster-builders*)) ;*monster-builders*から
                                     *monster-builders*)))                ;ランダムに関数を呼び出す
                      (make-array *monster-num*))))     ;モンスターの数だけの配列を作成
INIT-MONSTERS
CL-USER>

次はモンスターが倒されたかどうかを調べる関数です。
そしてその関数を使えば、全てのモンスターが倒されたかどうかも調べられるはずです。

CL-USER> (defun monster-dead (m)
           (<= (monster-health m) 0))
; 
; compilation unit finished
;   Undefined function:
;     MONSTER-HEALTH
;   caught 1 STYLE-WARNING condition
MONSTER-DEAD
CL-USER> (defun monsters-dead ()
           (every #' monster-dead *monsters*))
MONSTERS-DEAD
CL-USER>

また未定義の関数が出てきました。とはいえ名前からして、defstructで自動的に作られるものでしょう。
そしてmonsters-deadは*monsters*の全ての要素が死んでいるか調べるものですね。

次はモンスターを表示するための関数を書きます。

CL-USER> (defun show-monsters ()
           (fresh-line)
           (princ "Your foes:")
           (let ((x 0))
             (map 'list
                  (lambda (m)
                    (fresh-line)
                    (princ "   ")
                    (princ (incf x))
                    (princ ". ")
                    (if (monster-dead m)
                        (princ "**dead**")
                        (progn (princ "(Health=")
                               (princ (monster-health m))
                               (princ ") ")
                               (monster-show m))))
                  *monsters*)))
; 
; compilation unit finished
;   Undefined functions:
;     MONSTER-HEALTH MONSTER-SHOW
;   caught 2 STYLE-WARNING conditions
SHOW-MONSTERS
CL-USER>

xにまず0を格納し、敵モンスターが格納されている*monsters*から要素を取り出しながら
もし、そのモンスターが死んでいたら"**dead**"と出力し、
そうでなければ残り体力を表示し、monster-showでそいつについての詳細を表示する感じっぽいですね。

ここから各モンスターについて作っていくようです。
しかし結構長くなってしまいました。続きは次にします。