2011年6月22日水曜日

じゃんけんプログラム in Elisp

深夜になぜかEmacs Lispでジャンケンしようと思いたった…

いろいろな戦略を、たとえば相手の真似をする「反射戦略」や、グー・チョキ・パーを順番に出す「繰り返し戦略」などを、コールバック関数で渡す形で作る。いわゆるStrategyパターンと言ってよいだろうか。

メインの関数

;;; メインの関数
(defun janken (f1 f2)
  (switch-to-buffer "*janken-mode*")
  (let (first-val                       ;プレイヤー1の手
        second-val                      ;プレイヤー2の手
        result    ;勝敗を表す数(0: あいこ、1: P2の勝ち、2: P1の勝ち)
        history   ;手の履歴
        (vals '(g c p)))             ;手の選択肢(グー、チョキ、パー)
    (flet (                          ;ローカルな関数を定義
           (janken-prn-result ()     ;表示する関数
             (insert (format "%S : %S, %S\n"
                             first-val
                             second-val
                             (case result (0 'even) (1 'P2-win) (2 'P1-win)))))

           (janken-judge (v1 v2 f)                ;勝敗を判定する関数
             (if f (funcall f v1 v2)) ;事前処理。手の履歴を保存したり
             (destructuring-bind (x y)
                 (mapcar (lambda (v) (position v vals)) (list v1 v2))
               (% (+ (- x y) 3) 3))) ;三すくみの関係を表現する式

           (janken-loop ()                       ;ループする関数
              (while
                (zerop
                 (setf result
                       (janken-judge (setf first-val (funcall f1))
                                     (setf second-val (funcall f2))
                                     (lambda (x y) (setf history (cons (list x y) history))))))
                (janken-prn-result))
              (janken-prn-result)
              (message "%S" history) ;勝負がついた時点で*Messages*バッファに履歴を出力
              (when (y-or-n-p "Another game? ") ;継続するか問い合わせ
                (setf result nil)
                (janken-loop))))
      (janken-loop))))

なお、じゃんけんの「三すくみの関係」の表現方法はここに書いてあった:あるふぁ~版電算機部: 三すくみ。コードの(% (+ (- x y) 3) 3)に対応する。

戦略の関数

とりあえず5種類。

;手動入力
(defun janken-do-manually ()
  (intern (char-to-string (read-char "Press g or c or p: "))))

;ランダム戦略
(defun janken-do-randomly ()
  (elt vals (random (length vals))))

;繰り返し戦略(グー、チョキ、パーの順)
(defun janken-do-sequentially ()
  (elt vals (% (length history) 3)))

;pつまりパーを多めに出す戦略(確率 2/3)
(defun janken-do-weighted ()
  (let ((r (random 6)))
    (if (> r 2) (setf r 2))
    (elt vals r)))

;相手の最後の手を真似る戦略(反射戦略)
(defun janken-do-imitatively (firstp)
  (if history (funcall
               (lambda (x) (if firstp (second x) (first x)))
               (first history)) (janken-do-randomly)))

自由変数を含む形で書いているので(Elispなので動的スコープ)、メインの関数の中身と照らし合わせながら読まないと意味不明なはず。

実行例その1(手動 vs. ランダム戦略)

;次の式を評価する
(janken 'janken-do-manually 'janken-do-randomly)

;*janken-mode*バッファへの出力
g : c, P1-win
p : c, P2-win
p : g, P1-win
g : g, even
c : c, even
g : p, P2-win

実行例その2(パー多め戦略 vs. 反射戦略)

;次の式を評価する。反射戦略の関数は引数が要るので、ラムダ式を作る必要あり。
(janken 'janken-do-weighted (lambda () (janken-do-imitatively nil)))

;*janken-mode*バッファへの出力
p : g, P1-win
p : p, even
p : p, even
c : p, P1-win
p : c, P2-win
c : p, P1-win
p : c, P2-win
g : p, P2-win
p : g, P1-win

P2のほうはちゃんと真似している。これでよし。

…とまあこんな感じで、自動対戦させるプログラムくらいEmacs上ですぐに作れてしまうのであった。

0 件のコメント:

コメントを投稿