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

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

Common Lispを頑張る(57)

今日も「Land of Lisp」でダイス・オブ・ドゥームを作っていきます。
昨日まででルールエンジンの部分はできたので、プレイするための残りの部分をやっていきます。

人間同士の対戦機能

ゲームツリーを作る機能は完成しています。そこから人間同士でプレイするゲームにするのは簡単だそう。
プレイヤーの選んだ手に従ってゲームツリーを辿っていくだけでいいと、なるほど。

CL-USER> (defun play-vs-human (tree)
           (print-info tree)
           (if (caddr tree)
               (play-vs-human (handle-human tree))
               (announce-winner (cadr tree))))
; in: DEFUN PLAY-VS-HUMAN
;     (ANNOUNCE-WINNER (CADR TREE))
; 
; caught STYLE-WARNING:
;   undefined function: ANNOUNCE-WINNER

;     (HANDLE-HUMAN TREE)
; 
; caught STYLE-WARNING:
;   undefined function: HANDLE-HUMAN

;     (PRINT-INFO TREE)
; 
; caught STYLE-WARNING:
;   undefined function: PRINT-INFO
; 
; compilation unit finished
;   Undefined functions:
;     ANNOUNCE-WINNER HANDLE-HUMAN PRINT-INFO
;   caught 3 STYLE-WARNING conditions
PLAY-VS-HUMAN
CL-USER>

現在の状態およびそこから伸びる全ての手を含むゲームツリーを引数にとり、
print-infoで現在の盤面を表示し、次にゲームツリーのcaddr部分を調べ現状可能な手が残っているか、
残っているなら可能な手をhandle-humanで選択させ、
残っていなければannounce-winnerで決着のアナウンスといった感じでしょうかね。

それではまずprint-info関数を片づけます。

CL-USER> (defun print-info (tree)
           (fresh-line)
           (format t "current player = ~a" (player-letter (car tree)))
           (draw-board (cadr tree)))
PRINT-INFO
CL-USER>

ゲームツリーの構成さえ分かっていれば簡単な関数ですね。いや、実はそこが正直曖昧なんですが。

じゃあhandle-humanです。ここでは可能な全ての指し手を説明をつけて番号と一緒に表示し、
選んでもらうようにするようです。

CL-USER> (defun handle-human (tree)
           (fresh-line)
           (princ "choose your moce:")
           (let ((moves (caddr tree)))  ;可能な手をmovesへ
             (loop for move in moves    ;moveに1つずつ入れる
                   for n from 1         ;番号を振る
                  do (let ((action (car move))) ;actionは(自マス そこから攻撃可能なマス)となる
                       (fresh-line)
                       (format t "~a. " n)
                       (if action       ;可能な攻撃があれば
                           (format t "~a -> ~a" (car action) (cadr action))
                           (princ "end turn"))))
             (fresh-line)               ;全ての手の表示が終わったら改行して入力を待つ
             (cadr (nth (1- (read)) moves)))) ;選択された分岐のツリーを返す
HANDLE-HUMAN
CL-USER> 

そういえばここらはプレイヤーと接する部分だからしょうがないんですが不浄です。

勝者を宣言する機能は、清浄な部分と不浄な部分に切り分けることができます。
勝者を判定する部分だけを実装すればその関数は清浄に保つことができますね。
後の拡張を考え、プレイヤーの数に左右されないようにします。引分けになることもありますね。
そんな関数winnersを作ります。

CL-USER> (defun winners (board)
           (let* ((tally (loop for hex across board ;ゲーム盤を走査し各マスの所有者のリストを
                              collect (car hex))) 
                  (totals (mapcar (lambda (player) ;マスを所有しているプレイヤーがいくつマスを
                                    (cons player (count player tally))) ;所有しているか
                                  (remove-duplicates tally)))
                  (best (apply #'max (mapcar #'cdr totals)))) ;一位のプレイヤーの所有マスの数
             (mapcar #'car                                    ;勝者のリストを作る
                     (remove-if-not (lambda (x) ;totalsからマスの所有数が1位タイでない
                                      (not (eq (cdr x) best))) ;プレイヤーの要素を除外
                                    totals))))
WINNERS
CL-USER>

よし。あとはここで得た結果を教えてくれる不浄な関数を作るだけです。

CL-USER> (defun announce-winner (board)
           (fresh-line)
           (let ((w (winners board)))
             (if (> (length w) 1)
                 (format t "The game is a tie between ~a" (mapcar #'player-letter w))
                 (format t "The winener is ~a" (player-letter (car w))))))
ANNOUNCE-WINNER
CL-USER>

複数1位が出た時と1人の1位が出た時で出力する文章を変えているだけですね。

これで2人で遊べるダイス・オブ・ドゥームの完成です。
2人になったつもりで遊んでみます。

CL-USER> (play-vs-human (game-tree (gen-board) 0 0 t))
current player = a
  b-3 b-2 
 a-2 a-3 
choose your moce:
1. 3 -> 1
1
.
.
.
choose your moce:
1. end turn
1
current player = b
  a-2 a-1 
 a-1 a-1 
; Evaluation aborted on #<TYPE-ERROR expected-type: NUMBER datum: NIL>.
CL-USER>

いい感じだったんですが、announcei-winnerでplayer-letterを呼び出す時にnilが渡ってしまいました。
犯人として真先に疑うべきはwinnersですね。
全マスをプレイヤー1が占領した時に起きたのでそういうときどうなるのかな。

CL-USER> (winners #((1 2) (1 1) (1 1) (1 3)))
NIL
CL-USER>

むむむ。
先程の定義を見直します。

CL-USER> (defun winners (board)
           (let* ((tally (loop for hex across board ;ゲーム盤を走査し各マスの所有者のリストを
                              collect (car hex))) 
                  (totals (mapcar (lambda (player) ;マスを所有しているプレイヤーがいくつマスを
                                    (cons player (count player tally))) ;所有しているか
                                  (remove-duplicates tally)))
                  (best (apply #'max (mapcar #'cdr totals)))) ;一位のプレイヤーの所有マスの数
             (mapcar #'car                                    ;勝者のリストを作る
                     (remove-if-not (lambda (x) ;totalsからマスの所有数が1位タイでない
                                      (not (eq (cdr x) best))) ;プレイヤーの要素を除外
                                    totals))))
WINNERS
CL-USER>

tallyは(1 1 1 1)というリストになるはず。
totalsは…コンスセルのリストになるので、(1 . 4)。
bestは、当然4ですね。
...あ、remove-if-notじゃないのか。notとeqを使ってんだからそりゃそうだ。
修正して…再挑戦。

CL-USER> (play-vs-human (game-tree (gen-board) 0 0 t))
current player = a
  a-2 a-2 
 a-1 a-3 
The winener is a
NIL
CL-USER>

これはひどい。まあ、ちゃんとゲームが終了することは確認できたから運がいいのかな。

とりあえずここまで。今度は対戦相手を作ります。