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

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

Common Lispを頑張る(55)

新年あけましておめでとうございます。今年もよろしくお願いいたします。
それではやります。「Land of Lisp」15章、ダイス・オブ・ドゥームというゲームを作ります。

ダイス・オブ・ドゥーム

ルール

ゲームが発展していくにつれルールも難しくなるようですが、とりあえず最初のバージョンのルールです。
・プレイヤーは2人。六角形の升目のゲーム盤上でそれぞれ陣地を持ってスタート。
・各升目には、その陣地を所有するプレイヤーが六面サイコロをいくつか置く。
・プレイヤーは自分のターン中何回でも手を重ねていいが、最低1回は行動しなくてはいけない。
・どちらかのプレイヤーに指せる手が無くなった時点でゲーム終了。
・指す手は、隣合う的の陣地を攻撃すること。攻撃側の陣地にあるサイコロの数の方が多ければ攻撃可能。
・攻撃後の戦闘の勝敗は、現バージョンでは常に攻撃側の勝利とする。
・戦闘後、敗者のサイコロはマスから取り除かれ、勝者のマスにあったサイコロは1個を除き、奪ったマスに移動する。
・ターンの終了時、そのプレイヤーの軍に補給が行なわれる。
一番左上から右へ、その次の行の左から右へ、 というようにサイコロを1つずつ足していく。
足せるサイコロの数はそのターン、相手から奪ったサイコロの数マイナス1個。
・ゲーム終了時により多くのマスを所有していたプレイヤーの勝利。

バージョン1の実装

このゲームは関数型プログラミングで実装されます。つまり、清浄な関数型コードと
不浄な命令型コードから構築されるはずです。その2つの区別を明確にしながら進めてくれるそう。

まずは基本情報を保持するためのグローバル変数を定義。

CL-USER> (defparameter *num-players* 2)
*NUM-PLAYERS*
CL-USER> (defparameter *max-dice* 3)
*MAX-DICE*
CL-USER> (defparameter *board-size* 2)
*BOARD-SIZE*
CL-USER> (defparameter *board-hexnum* (* *board-size* *board-size*))
*BOARD-HEXNUM*
CL-USER>

ゲーム盤はリストで表現します。一番左上のマスが始点です。
そして各六角マスにあたるリストの要素に、マスの占有者、その場所にあるサイコロの数の情報を持たせます。
左上から、「プレイヤー1のマス・サイコロ2個、プレイヤー2のマス・サイコロ2個、
プレイヤー1のマス・サイコロ3個、プレイヤー1のマス・サイコロ1個」となっていた場合、

((0 2) (1 2) (0 3) (0 1))

と表わせるわけですね。自分の理解の確認のために詳しくやりすぎました。

で、今は2*2のゲーム盤だからいいような気がしますが、このゲーム盤のリストを配列でも
表わせるようにしておいた方が後々いいことになる気がします。
リスト後部へのアクセスの効率が悪いことは周知の事実ですね。
リストで表現されたゲーム盤を配列表現へと変換する関数を作成します。

CL-USER> (defun board-array (lst)
           (make-array *board-hexnum* :initial-contents lst))
BOARD-ARRAY
CL-USER>

これは清浄な関数ですね。よしよし。

ゲーム開始時にゲーム盤をランダムに初期化するための関数も作りましょう。

CL-USER> (defun gen-board ()
           (board-array (loop for n below *board-hexnum*
                             collect (list (random *num-players*)
                                           (1+ (random *max-dice*))))))
GEN-BOARD
CL-USER>

不浄ですね。randomとかいうやつがいます。
しかしこれ運が悪いとゲーム開始時に陣地を貰えないプレイヤーでませんかね。
とりあえず現時点で想定通りに動くか確認してみます。

CL-USER> (gen-board)
#((1 3) (0 1) (1 2) (0 2))
CL-USER> (gen-board)
#((1 3) (1 2) (0 2) (1 2))
CL-USER> (gen-board)
#((1 2) (1 3) (1 3) (1 3))
CL-USER>

…まあ、ちゃんと動いています。

0とか1とかはプレイヤーを区別するためのものとして優しくないので文字に変換できるようにします。

CL-USER> (defun player-letter (n)
           (code-char (+ 97 n)))
PLAYER-LETTER
CL-USER> (mapcar 'player-letter '(0 1))
(#\a #\b)
CL-USER>

code-charはASCIIコードを該当する文字に変換してくれるそう。ここも清浄ですね。

さて、配列を受け取って画面に綺麗に表示してくれる関数も欲しいです。
マスは六角形なのでちょっとずらして隣接するマスがわかりやすいとグッドですね。

CL-USER> (defun draw-board (board)
           (loop for y below *board-size* ;行数分の繰り返し
                do (progn (fresh-line)    ;まずは改行
                          (loop repeat (- *board-size* y) ;行ごとにずれるように
                               do (princ " "))
                          (loop for x below *board-size* ;列数分の繰り返し
                             for hex = (aref board (+ x (* *board-size* y))) ;対象のマスの要素を取り出す
                             do (format t "~a-~a " (player-letter (first hex))
                                        (second hex))))))
DRAW-BOARD
CL-USER>

画面への表示のための関数なので言うまでもありませんが不浄です。さて試してみます。

CL-USER> (draw-board #((1 3) (1 2) (0 2) (1 2)))
  b-3 b-2 
 a-2 b-2 
NIL
CL-USER>

うまいこと動いてくれている感じですね。

ここまででゲーム盤関連の箇所は完成のよう。
キリがいいようなので一旦ここで切ることにします。