Dice of Doom v4
最终版的Dice of Doom
这个游戏终于结束了,基本上这本书的主要内容也算看完了。实际上,我根本就写不出这些东西,源码也读的云里雾里。但这是一本很有趣的书,我想看完htdp之后用htdp推荐风格重新处理下这本书的代码,顺便也复习一下。
让Dice of Doom 更加的有趣
我们上回完成的版本虽然很棒,但总缺乏些什么。因为之前程序的设计,我们可以很轻易的扩展它,让它变得更有趣
先加载我们之前的游戏1
(load "dice-v3");
增加玩家数
因为我们之前的架构,增加玩家数很简单。为了增加玩家数后计算机能反应敏捷,让它变“傻”一点。
(defparameter *num-players* 4)
(defparameter *die-colors* '((255 63 63) (63 63 255) (63 255 63) (255 63 255)))
(defparameter *max-dice* 5)
(defparameter *ai-level* 2)
建立概率节点
我们本质上是向游戏树中加入几率节点,先升级attacking-moves函数,增加个board-attack-fail分支。
(defun attacking-moves (board cur-player spare-dice)
(labels ((player (pos)
(car (aref board pos)))
(dice (pos)
(cadr (aref board pos))))
(lazy-mapcan (lambda (src)
(if (eq (player src) cur-player)
(lazy-mapcan
(lambda (dst)
(if (and (not (eq (player dst) cur-player))
(> (dice src) 1))
(make-lazy (list (list (list src dst)
(game-tree (board-attack board cur-player src dst (dice src))
cur-player
(+ spare-dice (dice dst))
nil)
(game-tree (board-attack-fail board cur-player src dst (dice src))
cur-player
(+ spare-dice (dice dst))
nil))))
(lazy-nil)))
(make-lazy (neighbors src)))
(lazy-nil)))
(make-lazy (loop for n below *board-hexnum*
collect n)))))
board-attack-fail函数遍历所有位置。如果这个位置是攻击方就只能留下一个骰子
(defun board-attack-fail (board player src dst dice)
(board-array (loop for pos from 0
for hex across board
collect (if (eq pos src)
(list player 1)
hex))))
让骰子滚动起来
一下程序让一堆骰子滚动起来,并判断谁赢了
;rolling the dice
(defun roll-dice (dice-num)
(let ((total (loop repeat dice-num
sum (1+ (random 6)))))
(fresh-line)
(format t "On ~a dice rolled ~a. " dice-num total)
total))
(defun roll-against (src-dice dst-dice)
(> (roll-dice src-dice) (roll-dice dst-dice)))
我们还需要在游戏引擎中调用让骰子滚动的代码,选出正确的子树
(defun pick-chance-branch (board move)
(labels ((dice (pos)
(cadr (aref board pos))))
(let ((path (car move)))
(if (or (null path) (roll-against (dice (car path))
(dice (cadr path))))
(cadr move)
(caddr move)))))
更新handle-computer函数让它调用选择几率分支的函数
(defun handle-human (tree)
(fresh-line)
(princ "choose your move:")
(let ((moves (caddr tree)))
(labels ((print-moves (moves n)
(unless (lazy-null moves)
(let* ((move (lazy-car moves))
(action (car move)))
(fresh-line)
(format t "~a. " n)
(if action
(format t "~a -> ~a" (car action) (cadr action))
(princ "end turn")))
(print-moves (lazy-cdr moves) (1+ n)))))
(print-moves moves 1))
(fresh-line)
(pick-chance-branch (cadr tree) (lazy-nth (1- (read)) moves))))
同样我们可以处理handle-computer函数
(defun handle-computer (tree)
(let ((ratings (get-ratings (limit-tree-depth tree *ai-level*) (car tree))))
(pick-chance-branch
(cadr tree)
(lazy-nth (position (apply #'max ratings) ratings) (caddr tree)))))
升级AI
但现在还有个问题,AI还没有考虑骰子的滚动,依然认为骰子多的一定获胜。因此需要更新AI。
根据概率论原理,我们可以得到这样一个矩阵来表征攻击成功的概率
防守方\攻击方 | 2 | 3 | 4 | 5 |
---|---|---|---|---|
1 | 0.84 | 0.97 | 1.0 | 1.0 |
2 | 0.44 | 0.78 | 0.94 | 0.99 |
3 | 0.15 | 0.45 | 0.74 | 0.91 |
4 | 0.04 | 0.19 | 0.46 | 0.72 |
5 | 0.01 | 0.06 | 0.22 | 0.46 |
现在把这个概率交给计算机,更新get-ratings函数,给评分加权
(defun get-ratings (tree player)
(let ((board (cadr tree)))
(labels ((dice (pos)
(cadr (aref board pos))))
(take-all (lazy-mapcar
(lambda (move)
(let ((path (car move)))
(if path
(let* ((src (car path))
(dst (cadr path))
(odds (aref (aref *dice-odds*
(1- (dice dst)))
(- (dice src) 2))))
(+ (* odds (rate-position (cadr move) player))
(* (- 1 odds) (rate-position (caddr move)
player))))
(rate-position (cadr move) player))))
(caddr tree))))))
为了让修剪游戏树的函数能工作,还需要做些修改让它能处理新的游戏树。同时为了简化不再使用alpha-beta算法。
(defun limit-tree-depth (tree depth)
(list (car tree)
(cadr tree)
(if (zerop depth)
(lazy-nil)
(lazy-mapcar (lambda (move)
(cons (car move)
(mapcar (lambda (x)
(limit-tree-depth x (1- depth)))
(cdr move))))
(caddr tree)))))
;We mapcar across the tail of each move, so trimming is performed on
;both branches of any chance nodes.
改进增援方案
之前是依据消灭敌人多少来决定增援骰子数。现在我们换一种方法:依据占据的最大领地来决定增援多少。这使游戏的地缘性更加复杂。
回想我们之前在wumpus游戏里写过的相似函数
(defun get-connected (board player pos)
(labels ((check-pos (pos visited)
(if (and (eq (car (aref board pos)) player)
(not (member pos visited)))
(check-neighbors (neighbors pos) (cons pos visited))
visited))
(check-neighbors (lst visited)
(if lst
(check-neighbors (cdr lst) (check-pos (car lst) visited))
visited)))
(check-pos pos '())))
获得最大的集群,最后添加援军,注意,spare-dice仍然作为参数传递但已经不再被add-new-dice使用了。
(defun largest-cluster-size (board player)
(labels ((f (pos visited best)
(if (< pos *board-hexnum*)
(if (and (eq (car (aref board pos)) player)
(not (member pos visited)))
(let* ((cluster (get-connected board player pos))
(size (length cluster)))
(if (> size best)
(f (1+ pos) (append cluster visited) size)
(f (1+ pos) (append cluster visited) best)))
(f (1+ pos) visited best))
best)))
(f 0 '() 0)))
(defun add-new-dice (board player spare-dice)
(labels ((f (lst n)
(cond ((zerop n) lst)
((null lst) nil)
(t (let ((cur-player (caar lst))
(cur-dice (cadar lst)))
(if (and (eq cur-player player) (< cur-dice *max-dice*))
(cons (list cur-player (1+ cur-dice))
(f (cdr lst) (1- n)))
(cons (car lst) (f (cdr lst) n))))))))
(board-array (f (coerce board 'list)
(largest-cluster-size board player)))))
享受你的劳动成果吧
(serve #'dod-request-handleer)
疑问
我发现我可以选中任何自己的领地,只要上面的骰子数大于2。我可以攻击任何强大的敌人,而且一定能成功。不知道哪里的更改或错误!!
Footnotes
-
对从官网下载的源码和书中的源码可能需要修改,具体参见前几篇读书笔记。↩
blog comments powered by Disqus