12A猫で学んだこと-Memoir-

...What are you learning now?

プログラム練習ノート

こんにちは。StudentSです。
4月になりました。季節は春ですね。おひさまがぽかぽかしていて気持ちいいですね。
最近読んだ本の中にいいなぁ、と思ったフレーズがありました。
原文とは違うけれど、下記のような文章でした。

思い出って、おひさまの光に似ているね。心をあったかくしてくれる。
でも、抱きしめようとしても、重みを感じられないんだよね。

わたしの青春はいつくるんだろうなぁ? と思いながら、今日も何とか生きています...

1月に1度は何かブログをかけるようにしようと思っています。
Studentは、宿題やレポートの提出を迫られたら、例えネタがなかったとしても、
提出しなくちゃいけないんです。

最近、Pythonの練習にCheckIOというサイトで問題を解いています。
そして、上手い方々の答えを見て、勉強させてもらっています。
その中の1問に
Numbered triangles
という問題がありました。
問題の要旨としては、

  • 6枚の正三角形があり、それぞれの辺に数字がかいてある。
  • 6枚の正三角形をくっつけて正六角形を作る。ただし、くっつけることができる辺は同じ数字を書いてある辺だけ。
  • 作ることができる正六角形の辺の6つの数字(くっつけるのに使っていない数字)の和の最大値を求める。

という問題です。 この問題に恋に恋する人狼PLらしく、アタックしてみたいと思います。

パズルを解いてみる

それじゃあ、考えていこー よろしくね!
パズルも人狼も初手考えることは、全探索 だよね。
人狼なら自分以外を死亡させたら、基本的には自分の勝ちだし、
パズルだって、全てのケースを調べたら、問題は解ける。
吊り数や噛みの回数に余裕があったり、計算量的に可能なパズルだったら、まずそれだよね! このパズルを全探索するなら、1つの三角形の位置だけ固定させて、他の順番と向きを考えて... 考えなきゃいけないパターンは \( 5! \times (3!)^5 \times 3 \) になるかな?

factorial = lambda n: (lambda g,i:g(g, i))(lambda f, a: a if a <= 1 else f(f, a -1) * a, n)

簡単に階乗関数を使って計算してみると933120。これなら、全探索できそうだよね。
ちょっと効率の悪いスクリプトでも押し切れそうだね! ということで、愚直やってみよー!
クラスの女の子も、素直な男の子の方が扱いやすくて可愛いって言ってたしね!

def checkio(chips_input):
    pass

chips_inputにリストのリストで入力が入ってくるから、まずは6枚の三角形の並びを決めた時、スコアを計算する関数を書くね!

def calc_score(chips):
    """
    chip[i][0]: the length of hexagonal.
    chip[i][1]: counterclock's direction.
    chip[i][2]: anti-counterclock's direction.
    """
    #  chips[0][2] == chips[1][1] == ... chips[5][2] == chips[0][1]
    legal = all(chips[i][2] == chips[(i+1) % 6][1] for i in range(6))
    number_sum = sum(chip[0] for chip in chips)
    return legal * number_sum

とりあえず、まずはこんな感じかな? ルール違反の時は0にすればいいもんね。
ルールがはっきりすれば、後は全部のパターンを作るだけ。
「運命の人を見つけたら、後は愛するだけでいい」って言っていたのは誰だっけ?
授業で聞いた気がするんだけど、忘れちゃったなぁ。
好きな人をずっと好きでいるのが難しいのと同じで、やることが分かっていても実際に行うのは大変だよね。
ちょっと頑張ると、下みたいな感じでいいかな?

from itertools import permutations, product, chain
def checkio(chips_input):
    fixed = chips_input[0] # fixed triangle.
    chips = chips_input[1:] # triangles to be permutated.

    # For fixed triangle, only the length of hexagonal is fixed. (3, not3!.)
    fixed_patterns = [[fixed[li]] + [fixed[i] for i in range(len(fixed)) if i != li] for li in range(len(fixed))]
    concatenate_patterns = lambda chips: list(map(lambda fixed: [fixed] + chips, fixed_patterns))

    ps = map(lambda p:permutations(p), chips)  # Triangle Direction's patterns.  (3!)
    s = product(*ps)  # The group of triangles (whose direction's are not considered.)
    cand = map(permutations, s)  # Consider the order of triangles.
    # Generate the all patterns and check the maximum.
    result = 0
    for q in chain.from_iterable(cand):
        cands = concatenate_patterns(map(list, q))
        result = max(result, max(map(calc_score, cands)))
    return result

これで提出してみると、Task Solved!
でも、もう少し面白い感じにしたいよね...
このコードはエンターテイメントがないんだよね。
驚きやときめきあがないように感じる...
ボクなりにちょっと頑張るね!

from itertools import permutations, product, chain
checkio = lambda ci: max(max(map(lambda chs: all(chs[i][2] == chs[(i+1) % 6][1] for i in range(6)) * sum(ch[0] for ch in chs),
                            ([elem] + list(map(list, q)) for elem in ([ci[0][li]] + [ci[0][i] for i in range(3) if i != li] for li in range(3)))))
                         for q in chain.from_iterable(map(permutations, product(*map(permutations, ci[1:])))))

これが、ボクの答えだ! いつか運命の人に出会えることを祈った4-liner!

  • 2行目は答えを出す部分
  • 3行目は固定する三角形と他の三角形をくっつけるのを考える部分
  • 4行目は三角形の置き方を全て列挙する部分

これでどうかなぁ...ときめきがあるかな?
ちょっと細かいところで変なところもあるけれど、わくわくするようなそんなプログラムだといいなぁ。
少しずつ戸惑うかもしれないけれど、一緒にいて楽しい思い出をたくさん作れる...そんな素敵な人に出会いたいな。

速度改善について

これで正解はできたけれど...遅いんだよね...
短期村の人狼ゲームで人外の騙りとして大切なのは質の高い占い文を短時間で作り上げること
...速度が重要になるんだよね。  
遅い理由は、permutationsで、ルールに一致しない例を大量に作っているから。
permutationsで、例を作る時にルールに一致するかをチェックすれば、早くなるはず。
ということで、その関数を作ろうー!

from itertools import permutations, product, chain

predicator = lambda l, n: not l or l[2] == n[1]
def mperm(cands):
    def _inner(indices):
        if len(cands) == len(indices):
            yield tuple(cands[i] for i in indices)
        for i in range(len(cands)):
            if i not in indices and predicator(tuple(cands[indices[-1]]), tuple(cands[i])): 
                yield from _inner(indices + [i])
    for i in range(len(cands)):
        yield from _inner([i])


checkio = lambda ci: max(max(map(lambda chs: all(chs[i][2] == chs[(i+1) % 6][1] for i in range(6)) * sum(ch[0] for ch in chs),
                            ([elem] + list(map(list, q)) for elem in ([ci[0][li]] + [ci[0][i] for i in range(3) if i != li] for li in range(3)))))
                         for q in chain.from_iterable(map(mperm, product(*map(permutations, ci[1:])))))

ということで、itertools.permutations もどきを作って見たよ。
そして、要素を追加する時に、ルールをチェックするようにしたけれど、不格好だよね...
それに最悪計算量は変わらないし...
うーん、人狼も恋もプログラムも難しいね!

終わりに

ここまで読んでくれた人、ありがとー!
全探索の問題ってさ、人狼の詰み進行の時みたいに何も考えずにやっていくだけで頭をつかうことって少ないんだよね。
でも、人狼ゲームの結果が決まっているとしても、その中の過程がつまらないものになるか、面白いものになるかはプレイヤーたちの腕で決まると思っているよ!
このパズルの問題は全探索の問題だけど、その中の過程でなにかのときめきを作ることができていたら、ボクは嬉しいな!
また会うときがあったらよろしくね!

補足と反省

  • まずは問題作成者の方に感謝を。他の問題も含めてよい練習をさせていただいております。

  • 回答編にはすごい人達のコードがあるので、興味のある方は読まれることをおすすめします。先駆者の方々のコードをみたら、類似のアイデアはすでに上がっています。

  • 上記の回答は自分の中でかなり苦労して書いています...最初に正解にたどりついたコードは... (ときめきなんて、全然なかったよ!)

  • RPとネタのはさみ方が人狼と恋愛関係で半分ずつぐらいになっていて統一性がないなぁ。次はもう少し統一性を持たせないかな?

  • RPのキャラクターの幅をもう少し広くすることは今後の課題です。

What scenery did you see in villages, this Winter?

はじめに

視点整理, 情報整理人狼をする上では大切」と説く、人狼の熟練者の方がいました。
情報の整理、確かに重要な場面は多いと感じます。
詰み進行をみつけたり、他のPLの発言に応酬するために、情報整理は必須なのでしょう。

今回は、るる鯖の情報整理をやってみました。
間違いが含まれているかもしれませんが、ご了承ください。
指摘してくださるのはありがたいですが、だからといって処刑対象にするのは止めてください。

「情報の把握漏れ・把握ミスをするのは狂人の占い騙りだから? 違うよ! それはただ単に情報整理ができていないPLというだけ! PLとしての、実力の無さを露呈させているだけど、自分は真占いだよ!」

サマリー

「時間の無いゲーム中に、朝一の長文は読まれないから、簡潔に主張をまとめた方がいい」と人狼の熟練者が語っていました。
アドバイスに従って、簡潔に要点をまとめると下のようになります。

2017年 冬, るる鯖普通村12A猫の傾向について

  • るる鯖の村は半分近くが身内村
  • 普通村の中で12A猫の割合は12%程度
  • 期間中のGMさんの数は約50人。しかし、村の大半は少数のGMさんによって建てていただいている
  • 週末, 20時台 ~ 22時台の村が多い
  • 半分の12A猫の普通村は8分以内に始まる
  • ゲーム時間の平均は約35分

以上です。 呪殺が発生した時、偽占いかどうかを見極めるために占い文を精査する方は、下記の詳細をご覧ください。

調査対象

調査対象は以下の通り。

普通村と身内村の割合

はじめに、身内村と普通村の比率を調べます。 f:id:StudentS:20180316213039p:plain

普通村の方が多いと思っていたのですが、身内村の方が多かったです。
半分以上が身内村だったんですね...
以降、この4772村の普通村について見ていきます。

普通村配役の割合

普通村の中で一番遊ばれている配役を集計した結果が下になります。

f:id:StudentS:20180316212740p:plain

詳細テーブル

配役_0 村数_0 配役_1 村数_1 配役_2 村数_2 配役_3 村数_3 配役_4 村数_4
12B 854 19D猫 41 20Z 5 4A猫 2 7C猫 1
11A 741 13Z 38 17Z猫 5 8C猫 2 11D猫 1
12A猫 559 6B 37 10C猫 5 4C猫 1 30C猫 1
7C 425 5Z 31 4C 4 22Z猫 1 5A猫 1
8Z 249 5A 28 10Z猫 4 12C 1 7A 1
8C 190 17Z 28 22C猫 3 11D 1
17A 158 16A 26 5B 3 15A猫 1
4A 149 6C 22 9A 3 15C猫 1
18A猫 139 18Z猫 18 18D猫 3 6C猫 1
10Z 128 18Z 15 20Z猫 3 21D 1
6Z 110 19Z 12 22Z 3 15D猫 1
4Z 100 5C 10 17A猫 3 21C猫 1
12Z 81 9D 9 21Z 3 23D猫 1
7Z 78 14Z猫 9 12A 3 13C猫 1
11Z 65 6A 8 16B猫 2 18C猫 1
9Z 65 19Z猫 8 19C猫 2 8A 1
14Z 65 16Z猫 7 8B 2 11B 1
16Z 62 13Z猫 6 20C猫 2 15B猫 1
14D猫 57 12Z猫 6 23Z 2 8Z猫 1
15Z 44 11Z猫 5 21Z猫 2 23Z猫 1

コメント

  • 11Aよりも12Bの方が多いことは意外
  • 19D猫や18A猫は面白い配役だと思います! (呪殺対応が難しいけれど...)

以降、12A猫村の559村についてみていきます

GMさんと村数の関係性について

多くの村をGMさんが建ててくださっています。 最初に、GMさんと村数の関係をみます。

f:id:StudentS:20180317214241p:plain (仮GMさんの村はGMなしとして集計した)

統計値 村数
最大値 90
平均値 10.75
標準偏差 19.41
中央値 2.5
人数 52

コメント

グラフの横軸がGMさんが建ててくださった村の数。縦軸がGMさんの数です。
少数のGMさんが多くの村を建ててくださっていることが明確に分かります。
もう少し詳しくみると、村建て数が上位4人のGMさんだけで半分近くの村を建ててくださっているということが分かりました。
一番多いGMさんの村建て数は90村。1/6の村が1人のGMさんによって建られていたのですね....

GMさんの人数は52人。 私、StudentSも含め多くの方は一番左側の少ない村建て数のGMグループに属しますね。

村建てから開始までの時間

村が立ってからゲームが開始されるまでの時間を集計しました。 ログに「ゲーム開始時間」ということで、分単位で表示がされますが、ここでは秒単位で時間を取得したかったので、

  • [村建て開始時間] GMとして入村したメッセージが表示された時刻
  • [ゲーム開始時間] 「1日目の夜になりました。」というメッセージが表示された時刻

として、集計しました。

f:id:StudentS:20180318174742p:plain

統計値 秒数
最小値 30.0
最大値 6196.0
平均値 877.43
標準偏差 998.69
中央値 475.0

コメント

最も早く始まった村はなんと30秒!
* No.442024「12A猫番街」村
点呼すら不要の村だったようです。

一方、最も時間のかかった村は100分以上!
* No.447586「お気楽のんびり12A猫」村
時間帯によって、人の集まり方が違うというのをあらためて感じます。

中央値が475秒なので、半分の村は8分以内に始まるということですね。

ゲーム時間について

次に、ゲーム時間についてみます。 こちらも秒数単位で集計したかったので、時刻を使って集計しました。

f:id:StudentS:20180318175917p:plain

統計値 秒数
最小値 781.0
最大値 3261.0
平均値 2127.08
標準偏差 435.42
中央値 2155.0

コメント

正規分布に近い分布ですね。 最も時間が早い村は781秒(約13分)です。
* No.442020「12A猫番街」村
想像できると思いますが、1日目のグレランで引き分けになっています。 次に早い村は883秒(約15分)です。
* No.441663「12A猫が好きなGMの」村
これも、引き分け...だと予想していたのですが、村勝ちでした。
狼吊り → 猫噛み → 狼指定という、3Wが最短で死亡する進行でした。

逆に最も時間がかかった村は3261秒(約54分)
* No.439697「雨の日の12A猫」村
狩人の平和が2回あり、7日目まで続いていました。

そして、多くのゲームは35分 - 40分前後で終了するということが分かります。

村が建てられる時間帯

12A猫の普通村が建てられる時間帯を集計しました。 f:id:StudentS:20180318204334p:plain

詳細テーブル

時間帯 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日
0時台 4 7 5 8 6 8 6
1時台 1 1 0 2 1 2 2
2時台 0 0 1 0 0 0 0
3時台 0 0 0 0 0 0 0
4時台 0 0 0 0 0 0 0
5時台 0 0 0 0 0 0 0
6時台 0 0 0 0 0 0 0
7時台 0 0 0 0 0 0 0
8時台 0 0 0 0 0 0 0
9時台 0 0 0 0 0 0 0
10時台 0 0 0 0 0 1 2
11時台 0 1 0 0 1 5 2
12時台 0 1 0 0 1 4 6
13時台 0 0 0 0 0 2 2
14時台 0 0 0 0 0 2 4
15時台 0 0 0 0 0 3 7
16時台 0 0 0 1 0 4 7
17時台 0 0 0 0 0 3 9
18時台 0 0 0 0 1 4 9
19時台 0 0 0 3 4 6 13
20時台 6 4 8 14 11 14 12
21時台 11 12 11 12 17 19 14
22時台 17 21 18 16 21 20 15
23時台 18 15 19 13 16 13 10

コメント

  • 週末, 20時 ~ 22時台の村が多い
  • 朝、9時以前に村が建つことはほとんどない
  • 深夜、2時以降に村が建つことはほとんどない

使用CNセットについて

f:id:StudentS:20180318182708p:plain

詳細テーブル

CNセット_0 村数_0 CNセット_1 村数_1 CNセット_2 村数_2
全て 110 かくりよ 18 Q.E.D. 2
花畑 69 AAイラスト 15
Cathedral 53 シルエット 15
ジランドール 37 azuma 13
壱番街 36 あひる小屋 12
ランダムCNなし 33 メトロポリス 11
orange 29 ハロリンカ 10
わんだフル 29 トロイカ 10
あやかし 22 mtmt 8
shirone 21 KRNN 6

コメント

  • 全てが最も多い
  • 花畑とCathedralが2強
  • ランダムCNなしが意外と多い

おわりに

"ゲームの時間"を中心として集計してみました。
個人的に特に印象に残っていることは

  • GMさんが50人以上いること
  • 1ゲーム辺りの時間が30 ~ 40分程度であること
  • 10分以内に半分の村が始まること

ですね。特にGMさんの数が想像より多かったです。

Mouseless 12A -Playing only with keyboard-

はじめに

こんにちは、StudentSです。 自分は人狼を遊ばせていただく時、PCを使っています。
最近、現実世界で、キーボードだけでPCを操作する必要がある 状況になっています。
ふと、るる鯖で遊ぶ時に、マウスを極力使わないためにはどうすればいいかを考えました。
自分のPC環境はWindows7です。

サマリー

  • VIMを使って、キーボードだけで文章を入力できるようにする
  • Google Chrome / Vimium を使って、ブラウザ操作をキーボードで出来るようにする
  • Windows ショートカットの利用

想定環境

文章の入力について

テキスト・エディター

兎に角、これがなければはじまりません。 自分は人狼をするときには、sakuraエディタを使わせていただいていました。

好きなソフトです。 しかし、キーボードだけで文章を作るには、ちょっと厳しい。 キーボードで操作するといったら、 やはり...vim だろうということで使うことにします。 Emacs等でも出来ると思います。

Vimを配布してくださっているサイト https://www.kaoriya.net/からダウンロードさせていただき、使える状態まで学習していきます。

ブラウザ操作

  • ブラウザから、更新ボタンをクリックする必要がある
  • ブラウザをスクロールする必要がある
  • COの時などにボタンを押す必要がある

要するに、ブラウザの基本操作を出来る限りキーボードで操作することが必要と。

https://qiita.com/satoshi03/items/9fdfcd0e46e095ec68c1

上記サイト等の設定を参考にさせていただき、インストールします。 ChromeのショートカットキーボードとVimniumを使って、 ブラウザ操作が出来る状態まで学習していきます。

Windowsショートカット

さて、最後にテキスト入力とブラウザ操作の切り替えを素早くするためのWindowsショートカットを習得すればいいですね。

必須のショートカット

  • ALT + TAB: 開いているTabを切り替える
    • これでVimChromeを行ったりきたり
  • Windowsロゴキー + 左(右)方向キー: 画面の左側(右側)のウィンドウを最大化する
    • これでVimChromeを並べることが簡単に

残りは自分のPC操作に役立ちそうなものを少しずつ試してみるという感じでしょうか。

終わりに

さぁ、これで準備は整いました。 マウスを使わずに、るる鯖で遊ぶことができるはず!

...実際に遊んでみました。 確かに、マウスを使う頻度は半減しました。 しかし、朝の15秒の間に貼る文章を張り替えるまで、熟れていないです...
(マウス有りでも自分は苦手なことですが...)

あと、焦るとマウスに手が伸びる自分がいます。

「まずい...狩人を●にしてしまった...」
(狂人の時)「まずい...これは潜伏LWに誤爆だ...」

などとなって焦るとマウスに手が伸びてしまう傾向があります。 もう少し練習していきたいと思います。

雑多な話

音声入力...

音声入力は1つの作戦だと思いました。 Skype等で人狼を行う方がいらっしゃるので 1つの良い戦略だと思います。しかし、自分には厳しいそうです。 音声から文章への変換精度 という技術的な問題もあるのですが、それ以上に、自分の能力的な問題の方が大きいです。 話しことばをそのまま文字にしても、非常に読みにくい文章なってしまいます。

また、そもそもの話としてです、 音声入力は曲がりなりにも、書く内容を話さなくてはいけないわけです。 例えば No.296936 「今宵も12A猫」 の猫又CO (ちょっと改変)

猫又CO 「あたしはねこにゃーん」って、この年になるとちょっと恥ずかしくて言えないのよね。 30が少し見え始めたこの年に猫かぶって、媚びるような女に私はなりたくないんだよ。 若さが全てだとは思わないけどさ、やっぱり男達は若い子に惹かれやすいのはわかってる。 ・・・悔しいね。でも、プライドと自信は・・・まだ・・・なくさないよ。 こんな私が猫又です。

... 少し読むのは気恥ずかしいなぁ (そもそも、こんなことを書くなよとは言わないで...)

Vimの日本語入力

今の自分ではまだちょっと難しい。 半角全角の切り替えになかなか慣れませんね...

特に厳しいのが、

あ、12A猫なのに、狼が4匹いることになっちまったZE!
H☆A☆T☆A☆N しちまったYo!

などのように、半角・全角を織り交ぜて文章を打つときです。
ちょっと煩わしさを感じます。 日本に存在しているVimの修羅の方々は、様々な工夫をしておられるようです。 自分も少しずつ自分に合うやり方を見つけていくことが必要ですねー

最近、色々と無理やり感がありますが、何とかやっております。 3月がいい1ヶ月になりますように!

Werewolves' Zen of Python

こんにちは、StudentSです。 先月は恋愛的Python Zenを考えました。

Zen of Python by a maiden - 12A猫で学んだこと-Memoir-

今月は人狼Python Zenを考えてみました。
あ、先月みたいに17歳の高校生みたいな口調はやめます。
かなり苦しいですが、掲載します。

Werewolves' Zen of Python

Beautiful is better than ugly.

発言は読みやすくしましょう。

Explicit is better than implicit.

人外候補への誘導ははっきりとしましょう。

Simple is better than complex. Complex is better than complicated.

狼の戦略は単純な方がいい。
複雑な戦略を採用する時は、1日で全てを実行するものではなく、
全日に渡って、単純な戦略を絡み合わせて遂行できるものにしよう。

Flat is better than nested.

大量のアンカーの応酬は見にくい。

Sparse is better than dense.

朝一で長文を投下するのではなくて、発言を散逸させよう。

Readability counts.

他のプレイヤーの役職を読もう。

Special cases aren't special enough to break the rules. Although practicality beats purity.

奇策は貴方が思うほど珍しくはない。
そして、作戦は美しさや一貫性よりも、実用性で評価されるべき。

Errors should never pass silently. Unless explicitly silenced.

他のプレイヤーのミスや把握漏れは、明確に指摘しよう。
そのプレイヤーが確定村陣営でない限りはね。

In the face of ambiguity, refuse the temptation to guess.

確定情報を手にいれることができない時、 推測を確定情報と同一視することは避けたほうがよい。

There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch.

占い師はたった1人。でもGMじゃないと、誰が占い師か最初はわからない。

Now is better than never. Although never is often better than right now.

村認定出来ないPLは早く処刑した方がいい。しかし、思考停止で即時処刑するよりも
狐や狼の数などを考えて、絶対に処刑しないほうが良いことも多い。

If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea.

勝率を高めるためには、人狼の戦略は単純な方がいい。
仲間の狼に説明しにくい戦略は高勝率を達成できない 。
仲間の狼に説明しやすい戦略は、いい戦略の必要条件を満たしている。

Namespaces are one honking great idea -- let's do more of those!

呪殺対応の時に、名前の情報を使うのはいいアイデアだね。

苦しいですねー ... 寒い日が続きますが、お体に気をつけて!

Zen of Python by a maiden

はじめに

お久しぶりです。StudentSです。寒い時期になってきましたねー

自分は12Aの村に参加させていただく時、 全く違った分野の話題を人狼の言葉に置き換えることがあります。 ランダムCNで与えられた名前と人狼の話題と結びつけるのは面白いです。

さて、今回はそれと似たことを他の分野でやってみるーという軽いノリのエントリです。

"Zen of Python" というものがあります。

PEP 20 -- The Zen of Python | Python.org

Pythonというプログラミング言語を使う時に大切な考え方をまとめた項目です。
Pythonの実行環境がある方は、

import this 

と入力することで、読むことができます。

Zen of Python by Love Maiden

この手の話をするときはちょっと堅さは捨てたほうがよさそうだねー だから、ちょっと口調をかえるね。 拙いけれど、私が訳してみたZen of Python. ちょっとでもくすりと笑ってくれるとうれしいな!

Beautiful is better than ugly.

綺麗な女の子の方が好きだよね?

Explicit is better than implicit.

好きっていうのは、はっきり口にしないと伝わらないよ。

Simple is better than complex. Complex is better than complicated.

恋は直球勝負。 「人の心は複雑だから分かりあえない」なんて言わないで。
色々あると思うけれど、君の事が知りたいんだよ。

Flat is better than nested.

君と同じところに立ちたいんだ。

Sparse is better than dense.

たまーに会えるこの時間が大切なんだよ。

Readability counts.

不機嫌になると、すぐ顔にでる所も可愛いと思うよ。

Special cases aren't special enough to break the rules. Although practicality beats purity.

君と一緒に世界を広げていくことは、特別な事じゃなくなった。
私の心は綺麗なだけじゃないけれど、これからも一緒に楽しい思い出を作ろうね。

Errors should never pass silently. Unless explicitly silenced.

言ってよ。何か言いたいことがあるならちゃんと。
でも、静かに横にいて欲しいときもあるんだけどね。

In the face of ambiguity, refuse the temptation to guess.

私のことを勝手に分かったつもりにならないで。

There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch.

たった1つの大切なこと。はっきりしてるよ。
最初は全然こんな風になるなんて思わなかったけどね。

Now is better than never. Although never is often better than right now.

目先のことに目を奪われちゃいけないけれど、
私にとっては"今"が大切なの。

If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea.

恋って単純な方がいいんだよね。きっと。

Namespaces are one honking great idea -- let's do more of those!

君ともっと名前で呼び合いたいな。ねぇ、もっと私の名前を呼んでよ。

ひどいのもいくつか混じっているね...でも、まぁ許して。

Ask forgiveness not permission.

好きになってもいいなんて聞かないで。好きになってから考えてよ。

って、誰かも言ってたしー 正直さ、幼馴染のあいつのこともPythonもまだ良く分かっていないんだけど、 少しずつでいいから、知りたいな。

終わりに

こんな感じでいいのでしょうか。良くない気がしますね...
RPの雑さや無理や原文をかなり無視した意訳。
これが今の自分の力ですか...うーん、厳しい。

まぁ、何であってもやってみないと駄目なんでしょうね。

  • たとえ吊られても話さないと人狼ゲームをやっている感じがしない。
  • プログラミング言語は何か書いてみないと始まらない。
  • 恋愛関係が全てとは思わないけど、他人との関係を意識しないと、生きる世界が狭くなってしまう

人から見れば、簡単で単純なことかもしれないけれど、自分ができるようになりたいと願い、達成できたという経験...
そういうものを大切にしながら、来年も過ごせたらなぁと思っています。

とりあえず、少し風邪気味のこの時期、自分はゆっくり休もうと思います。

寒い日が続くのでどうか皆様ご自愛ください... 忙しない時期ですが、どうかお体に気をつけて。

Werewolves’ Puzzle Auto-Generator 人狼パズル自動生成[一応完成版]

はじめに

Happy Halloween!
こんにちは、StudentSです。 先月から作ってみた人狼パズル自動生成の仕組みが一応出来ました。 大体、やってみたいことはできたかなぁと思っています。 ソースコードGithub の方にアップロードしています。

このエントリのまとめ

使い方

Githubに書いている通りですが、

python puzzle_generator.py -lang jp

で問題が生成されます。

プレイヤーの数を指定したいときは、-v, -w, -lのオプションで指定できます。

日本語で出力したいときは、 -lang jpを付記します。

反復アルゴリズムで問題を生成しています。 一定回数の反復で問題が生成できなかったときは、 例外が発生するのでご容赦ください。(RuntimeException)

詳しいことは、

python puzzle_generator.py -h 

でみることが出来ます。

主な改善点

  • 狂人の役職を追加
  • 出力される問題の傾向の変更が可能になった

ルール

  • 人狼のプレイヤーを探し出すことが目的
  • プレイヤーは他のプレイヤーについて、狼かどうかを主張する。
  • 村陣営は決して嘘をつかない。
  • 狼と狂人は、嘘をつくかもしれない。

生成される問題と答えの例

いくつか問題を掲載します。

問題1 (初級編)

  • 内訳:村陣営/狼/狂=3/1/1, PL:A-E

各PLの主張

  • Aの主張:B○,D○,E○
  • Bの主張:E●
  • Cの主張:E●
  • Dの主張:E○

解答

  • 狼:C

問題2 (中級編)

  • 内訳:村陣営/狼/狂=4/1/2, PL:A-G

    各PLの主張

  • Aの主張:B○,C●,D○,E○,G○
  • Bの主張:C○,E○,F○
  • Cの主張:A○,F○,G○
  • Dの主張:B●,E○,F○
  • Eの主張:B○,C●,D○
  • Fの主張:C○,E○
  • Gの主張:D○,F○

    解答

  • 狼:B

問題3 (上級編)

  • 内訳:村陣営/狼/狂=5/1/3, PL:A-I

    各PLの主張

  • Aの主張:C○,E○,F●,G○,H○
  • Bの主張:A○,E○,G○,H○
  • Cの主張:B○,D○,G●,H○
  • Dの主張:A○,F●,G○,H○
  • Eの主張:A○,B○,F○
  • Fの主張:B○,C●
  • Gの主張:C○,I○
  • Hの主張:E○,I○
  • Iの主張:G●

    解答

  • 狼:F

感想

  • 実際の村では大切な結果を出す時系列の情報が抜け落ちている。
  • 自分で解いてみると、想像より問題が難しかった。
  • 自分自身の「作れた」という経験は大切にしたい...

Werewolves’ Puzzle Auto-Generator 人狼パズル自動生成[プロトタイプ]

このエントリのまとめ

  • 人狼を題材にしたにパズルを考えます。
  • パズルの問題と解答の自動生成をやってみました。

問題

  • 村陣営は決して嘘をつかない。
  • 狼は嘘をつくかもしれない。 さて、狼のPLは誰?

内訳:村陣営/狼=4/2, Players: A~F

各PLの主張

  • Aの主張:C●,F●
  • Bの主張:D○,E●
  • Cの主張:E●

凡例

  • Aは、CとFを狼だと主張している。
  • Bは、Dを狼ではないと主張し、Eを狼だと主張している。
  • Cは、Eを狼だと主張している。

解答

Wolves=A,E (狼はAとE)

解説

  • Aが狼陣営の証明

Aが村陣営だと仮定すると、 狼はC, Fになる。自動的にB・Eが村陣営となるけれど、BがEが狼と主張しているから矛盾。 だから、Aは嘘をついている。狼。

  • Eが狼陣営の証明

Eが村陣営だと仮定すると、B, Cが狼になってしまうけれど、 それだと狼数が3になってしまうので、あり得ない。 従って、Eは狼。

こんなパズルを自動で作る仕組みができないかなぁ...と思って、 まずは試しにプロトタイプを作っています。 「解説」を作ることは難しいけれど、それ以外の部分は以外と単純な仕組みで出来ないかなぁ... と思ってやってみました。

自動生成した数問の問題は以下のようになります。

問題1

内訳:村陣営/狼=4/2, PL:A~F

各PLの主張

  • Aの主張:B●,D○
  • Bの主張:E○,F○
  • Cの主張:A●,F○
  • Dの主張:F●

解答

Wolves=A,D

解説

  • Aが狼の証明 Aが村だと仮定すると、B・Cが狼となる。 しかし、DがFが狼と主張しているから矛盾。

  • Dが狼の証明 Dが村だと仮定すると、A・Fが狼となる。 しかし、Bが嘘をついていることになるので矛盾。

問題2

内訳:村陣営/狼=4/2, PL:A~F

各PLの主張

  • Aの主張:C○,F●
  • Bの主張:C●
  • Cの主張:B●
  • Dの主張:A○
  • Eの主張:B●

解答

  • Wolves=B,F

問題3

内訳:村陣営/狼=5/3, PL:A~H

各PLの主張

  • Aの主張:B●,F●
  • Bの主張:D●,G●
  • Cの主張:E○
  • Dの主張:A○
  • Eの主張:H●

解答

  • Wolves=B,F,H

解説

  • Bが村だと仮定すると、A・D・Gが狼となる。しかし、Eの主張より矛盾。
  • Fが村だと仮定すると、B・A・Dが狼となる。しかし、Eの主張より矛盾。
  • Hが村だと仮定すると、B・F・Eが狼となる。しかし、Cの結果より矛盾。

こんな感じです。

自分で解いていないですが... 次のような問題も作ってくれました。 (合っているか自信がない....)

問題4

内訳:村陣営/狼=10/5, PL:A~O

各PLの主張

  • Aの主張:F●,L●
  • Bの主張:G○,O●
  • Cの主張:G○
  • Dの主張:O○
  • Eの主張:F●
  • Fの主張:I○
  • Gの主張:K●
  • Hの主張:J●
  • Iの主張:J○
  • Jの主張:L●
  • Kの主張:I●
  • Lの主張:N○

解答

Wolves=B,F,H,K,L

やりたいことや希望....

  • リファクタ(ソースコードの整理)はしたいなぁ...

  • 狂人が入っている内訳は作りたいなぁ...

  • 120%自己満足で作っていますが、何かコメントを頂けると嬉しいなぁ...

おまけ

恥ずかしさを感じつつ、ソースコードを貼付けておきたいと思います。 全然整理ができていないので、コメント等々おかしい箇所が多いですね... 今年の秋 ~ 冬にかけて、自分の力量と相談しながら、満足のいく所までやってみたいなー と思っています。

""" Create Werewolve's Puzzle.  

"""

wolf_number = 5
village_number = 10

import os, glob
from itertools import combinations
import random
import numpy as np
from collections import namedtuple

class FResult(object):
    def __init__(self):
        pass
    @classmethod
    def get_id(cls):
        raise SyntaxError("Please Implement.")

class White_Result(FResult):
    def __init__(self):
        pass
    @classmethod
    def get_id(cls):
        return "white"

class Black_Result(FResult):
    def __init__(self):
        pass
    @classmethod
    def get_id(cls):
        return "black"



class Player(object):
    """ Abstract class for players.
    """
    def __init__(self, index):
        self.result = dict()
        self.index = index
        self.result[index] = White_Result.get_id()
    
    @classmethod
    def get_name(cls):
        raise SyntaxError("Please implement this function.")
    
    def add_result(self, index, result_name):
        self.result[index] = result_name

    def delete_result(self, target_index):  
        assert target_index in self.result
        assert target_index != self.index 

        del self.result[target_index]

    def refer_result(self):
        return self.result

    def has_result(self):
        return len(self.result) > 1

    def display_index_result(self, lang="en"):
        def _index_to_alphabet(number):
            return chr(ord("A") + number)
        indices = sorted(self.result.keys())
        indices = [index for index in indices if index != self.index]
        result_list = list()
        for index in indices:
            alphabet = _index_to_alphabet(index)
            result_id = self.result[index]
            if result_id == White_Result.get_id():
                result_list.append("{0}{1}".format(alphabet, "○"))
            elif result_id == Black_Result.get_id():
                result_list.append("{0}{1}".format(alphabet, "●"))
            else:
                raise ValueError(result_id)
        my_alphabet = _index_to_alphabet(self.index)

        if lang == "en":
            claim = "{0}'s claim".format(my_alphabet)
        elif lang == "jp":
            claim = "{0}の主張".format(my_alphabet)

        return "{0}:".format(claim) + ",".join(result_list)

    def replace_result_ids(self, replace_map):
        """ Replace the result and own ids.

        :param replace_map: prev_id: next_id.
        """
        self.index = replace_map[self.index]
        revised_result = dict()
        for prev_id, result_id in self.result.items():
            next_id = replace_map[prev_id]
            revised_result[next_id] = result_id
        self.result = revised_result
        self.result[self.index] = White_Result.get_id()
        

class Wolf(Player):
    """ Wolf.
    """
    def __init__(self, index):
        super(Wolf, self)
        self.result = dict()
        self.result[index] = White_Result.get_id()
        self.index = index

    def get_name(self):
        return "wolf"
    pass

class Forseener(Player):
    """ Forseener.
    """
    def __init__(self, index):
        super(Forseener, self)
        self.result = dict()
        self.result[index] = White_Result.get_id()
        self.index = index

    @classmethod
    def get_name(cls):
        return "forseener"

def generate_answer(f, w):
    """ Generate the answer.

    :param f: the number of forseener. 
    :param w: the number of wolf. 
    :return: the list of Players.  
    """
    answer_list = list()
    answer_list += [Forseener(index) for index in range(f)] 
    answer_list += [Wolf(len(answer_list) + index ) for index in range(w)] 
    return answer_list


def is_result_coherent(index_to_person, forseener_indices, wolf_indices):
    def _coherent_check(p_result_dict): 
        for index_key, name_value in p_result_dict.items():
            if index_key in wolf_indices:
                if name_value != Black_Result.get_id():
                    return False
            elif index_key in forseener_indices:
                if name_value != White_Result.get_id():
                    return False
            else:
                raise ValueError("Invalid", index_key)
        return True


    assert len(forseener_indices) == village_number
    assert len(wolf_indices) == wolf_number

    for f_index in forseener_indices:
        f_result = index_to_person[f_index].result
        if _coherent_check(f_result) is False:
            return False
    return True

def get_coherent_cases(index_to_person):
    """ Return the cases where the results are coherent.

    :param index_person: :obj:`dict`. 
    :return :obj:`list`: whose elem is :obj:`dict`.
                - refer to the **wolf_indices** and *forseener_indices*.
    """

    coherent_ret_list = list()
    indices = list(range(wolf_number + village_number))
    for candidate in combinations(indices, wolf_number):
        wolf_indices = [index for index in indices if index in candidate]
        forseener_indices = [index for index in indices if index not in candidate]

        if is_result_coherent(index_to_person, forseener_indices, wolf_indices):
            row = dict()
            row["wolf_indices"] = wolf_indices
            row["forseener_indices"] = forseener_indices
            coherent_ret_list.append(row)
    return coherent_ret_list

def random_add_result(index_to_person):
    """ Strategy to restrict the possibility. 
    """
    from_target_indices = [index for index, person in index_to_person.items()
                          if len(person.result) < wolf_number + village_number]
    from_index = random.choice(from_target_indices)
    from_person = index_to_person[from_index]
    current_indices = from_person.result.keys()
    to_target = [index for index in index_to_person.keys() if from_index != index
            and index not in current_indices]
    to_index = random.choice(to_target)
    if 0 <= random.random() <= 1/2:
        index_to_person[from_index].add_result(to_index, White_Result.get_id())
    else:
        index_to_person[from_index].add_result(to_index, Black_Result.get_id())
    return index_to_person

def random_delete_result(index_to_person):

    """ Strategy to loose the restriction.  
    """
    # The target should have result except himself.  
    target_indices = [index for index, person in index_to_person.items() if len(person.result) > 1]

    from_index = random.choice(target_indices)
    result_indices = list(index_to_person[from_index].result.keys())
    to_target = [index for index in result_indices if from_index != index]
    to_index = random.choice(to_target)

    index_to_person[from_index].delete_result(to_index)

    return index_to_person


def assort_person_id(index_to_person):
    """ Sort the person's id, by the order of the number of claims. 

    :return: the changed index_to_person.
    
    """
    indices = index_to_person.keys()
    indices = sorted(indices,
                    key=lambda index: len(index_to_person[index].result.keys()),
                    reverse=True)

    replace_map = {original_id: index for index, original_id 
                   in enumerate(indices)}  

    revised_index_to_person = dict()
    for prev_index, person in index_to_person.items():
        person.replace_result_ids(replace_map)

        next_index = replace_map[prev_index]
        revised_index_to_person[next_index] = person
    
    return revised_index_to_person

def _index_to_alphabet(index):
    return chr(ord("A") + index)

def display_problems(village_number, wolf_number, index_to_person, lang="en"):
    text_lines = list()
    persons = len(index_to_person)
    if lang == "en":
        first_line = "## Problem"
        second_line = "Roles:Villagers/Wolves={v}/{w}, PL:{first_pl}~{last_pl}"\
                     .format(v=village_number, 
                             w=wolf_number, 
                             first_pl='A',
                             last_pl=_index_to_alphabet(persons-1))
        third_line = "### Player's claims"
    elif lang == "jp":
        first_line = "## 問題"
        second_line = "内訳:村陣営/狼={v}/{w}, PL:{first_pl}~{last_pl}"\
                     .format(v=village_number, 
                             w=wolf_number,
                             first_pl='A',
                             last_pl=_index_to_alphabet(persons-1))

        third_line = "### 各PLの主張"
    else:
        raise ValueError("Specification of lang is invalid.", lang) 
    text_lines += [first_line, second_line, third_line]

    indices = sorted(index_to_person.keys(),
                    key=lambda index: len(index_to_person[index].result),
                    reverse=True)
    for index  in indices:
        person = index_to_person[index]
        if person.has_result():
            text_lines.append(person.display_index_result(lang))
    return "\n".join(text_lines)

def display_answers(answer_row, lang="en"):
    def _index_to_alphabet(index):
        return chr(ord("A") + index)

    text_lines = list()
    villagers = [_index_to_alphabet(elem) for elem in sorted(answer_row["forseener_indices"])]
    wolves = [_index_to_alphabet(elem) for elem in sorted(answer_row["wolf_indices"])]
    if lang == "en":
        first_line = "## Answer  "
        wolf_line = "Wolves={0}".format(",".join(wolves))
    elif lang == "jp":
        first_line = "## 解答  "
        wolf_line = "Wolves={0}".format(",".join(wolves))
    else:
        raise ValueError("Invalid lang", lang)
    
    text_lines += [first_line, wolf_line]
    return "\n".join(text_lines)


if __name__ ==  "__main__":
    lang = "jp"
    result = generate_answer(wolf_number, village_number)
    index_to_person = {index:elem for index, elem in enumerate(result)}

    max_iteration = 100
    for iter_number in range(max_iteration):

        ret_list = get_coherent_cases(index_to_person)

        for row in ret_list:
            wolf_indices = row["wolf_indices"]
            forseener_indices = row["forseener_indices"]

        #print("length", len(ret_list))

        if len(ret_list) == 0:
            random_delete_result(index_to_person)
        elif len(ret_list) > 1:
            random_add_result(index_to_person)
        else:
            break


    index_to_person = assort_person_id(index_to_person)
    ret_list = get_coherent_cases(index_to_person)
    #assert len(ret_list) == 1
    answer_row = ret_list[0]
    #print(answer_row) 
    texts = display_problems(village_number, wolf_number, index_to_person, lang)
    print(texts);
    texts = display_answers(answer_row, lang)
    print(texts);exit(0)
    str_list = list()