前言

终于,我们来玩狼人杀了,完整代码见 https://github.com/zong4/AILearning。

游戏规则

规则的话,因为人越多越复杂,所以我这边就只弄了一个丐版的,五个人:三村民,一狼人,一预言,先给大家看一眼初始化游戏的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
ROLES = ['citizen', 'werewolf', 'prophet']

class Game:
def __init__(self):
number = 5
roles = {'citizen': 3, 'werewolf': 1, 'prophet': 1}

players_name = ['player' + str(i) for i in range(number)]
# self.symboles = [Symbol(player_name + ' is ' + role) for player_name in players_name for role in ROLES]
knowledge_base = And(
Or(Symbol('player0 is citizen'), Symbol('player0 is werewolf'), Symbol('player0 is prophet')),
Not(And(Symbol('player0 is citizen'), Symbol('player0 is werewolf'))),
Not(And(Symbol('player0 is citizen'), Symbol('player0 is prophet'))),
Not(And(Symbol('player0 is werewolf'), Symbol('player0 is prophet'))),

Or(Symbol('player1 is citizen'), Symbol('player1 is werewolf'), Symbol('player1 is prophet')),
Not(And(Symbol('player1 is citizen'), Symbol('player1 is werewolf'))),
Not(And(Symbol('player1 is citizen'), Symbol('player1 is prophet'))),
Not(And(Symbol('player1 is werewolf'), Symbol('player1 is prophet'))),

Or(Symbol('player2 is citizen'), Symbol('player2 is werewolf'), Symbol('player2 is prophet')),
Not(And(Symbol('player2 is citizen'), Symbol('player2 is werewolf'))),
Not(And(Symbol('player2 is citizen'), Symbol('player2 is prophet'))),
Not(And(Symbol('player2 is werewolf'), Symbol('player2 is prophet'))),

Or(Symbol('player3 is citizen'), Symbol('player3 is werewolf'), Symbol('player3 is prophet')),
Not(And(Symbol('player3 is citizen'), Symbol('player3 is werewolf'))),
Not(And(Symbol('player3 is citizen'), Symbol('player3 is prophet'))),
Not(And(Symbol('player3 is werewolf'), Symbol('player3 is prophet'))),

Or(Symbol('player4 is citizen'), Symbol('player4 is werewolf'), Symbol('player4 is prophet')),
Not(And(Symbol('player4 is citizen'), Symbol('player4 is werewolf'))),
Not(And(Symbol('player4 is citizen'), Symbol('player4 is prophet'))),
Not(And(Symbol('player4 is werewolf'), Symbol('player4 is prophet'))),

Or(Symbol('player0 is citizen'), Symbol('player1 is citizen'), Symbol('player2 is citizen'), Symbol('player3 is citizen'), Symbol('player4 is citizen')),
Or(Symbol('player0 is werewolf'), Symbol('player1 is werewolf'), Symbol('player2 is werewolf'), Symbol('player3 is werewolf'), Symbol('player4 is werewolf')),
Or(Symbol('player0 is prophet'), Symbol('player1 is prophet'), Symbol('player2 is prophet'), Symbol('player3 is prophet'), Symbol('player4 is prophet')),
)


# random roles
self.players = []
for player_name in players_name:
role = random.choice(ROLES)
roles[role] -= 1

if roles[role] == 0:
ROLES.remove(role)

if role == 'citizen':
player = Citizen(player_name, role)
elif role == 'werewolf':
player = Werewolf(player_name, role)
elif role == 'prophet':
player = Prophet(player_name, role)

player.add_knowledge(knowledge_base, None, True)
self.players.append(player)

其中,knowledge_base 是每个人都有的基础知识,相信大家看过上一篇之后,一定懂为什么 knowledge_base 长这样。

然后是游戏流程,天黑 -> 狼人刀人 -> 预言查人 -> 各自发言 -> 各自投票 -> 下一天,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def run(self):
day = 0
while True:
day += 1
print('Day', day)

dead = None
for player in self.players:
if player.get_role() == 'werewolf':
dead = player.kill(self.players)
print(dead.get_name() + ' is dead')

if len(self.players) == 2:
print('Werewolf win')
break

symbol = Symbol(dead.get_name() + ' is ' + dead.get_role())
for player in self.players:
player.add_knowledge(symbol, None, True)
print(symbol)
print()

prophet = None
checked = None
for player in self.players:
player.say(self.players.index(player))
if player.get_role() == 'prophet':
prophet = player
checked = player.check(self.players)
print()

symbol1 = Symbol(prophet.get_name() + ' is ' + prophet.get_role())
symbol2 = Symbol(checked.get_name() + ' is ' + checked.get_role())
for player in self.players:
player.add_knowledge(symbol1, None, True)
player.add_knowledge(symbol2, None, True)

votes = []
for player in self.players:
votes.append(player.vote(self.players))
print()

target = max(set(votes), key = votes.count)
self.players.remove(target)
print(target.get_name() + ' is voted')
print()

if target.get_role() == 'werewolf':
print('Citizen win')
break

角色实现

主要给大家讲一下基类 Player 吧,剩下的大家可以自行实现,代码如下,完整代码见 https://github.com/zong4/AILearning。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Player:
def __init__(self, name, role):
self.name = name
self.role = role

self.knowledge = And(
Symbol(self.name + ' is ' + self.role),
)

def get_name(self):
return self.name

def get_role(self):
return self.role

def add_knowledge(self, symbol1, symbol2, sure):
if sure:
self.knowledge = And(self.knowledge, symbol1)

if symbol2:
self.knowledge = And(self.knowledge, Implication(symbol1, symbol2))

def get_knowledge(self):
return self.knowledge

def get_probabilities(self, players, target):
probabilities = []
for player in players:
symbol = Symbol(player.get_name() + ' is ' + target)
model_check(self.knowledge, symbol)
probabilities.append((player, symbol.true / (symbol.true + symbol.false)))

logger.info(self.knowledge)
logger.info(probabilities)

probabilities.sort(key = lambda x: x[1])
return (probabilities[0], probabilities[-1])

其中最重要的就是这个 get_probabilities 了,它主要是根据当前玩家所有的 knowledge,通过 model_check 来判断其余每个玩家为 target 的概率,然后返回概率最低的和最高的两个。

如果你还不了解 model_check,可以先阅读这一篇

那基于这个函数我们就能让好人去投票最像狼人的人,也能让预言家去查最像狼人的人,同时也可以让狼人去杀最像预言家的人。

游戏结果

这里给大家随便看一局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Day 1
player2 is dead
player2 is citizen

player0 is prophet
player0 checked player4 is citizen
player1 is citizen(lying)
player3 is citizen
player4 is citizen

player0 voted player1
player1 voted player0
player3 voted player1
player4 voted player1

player1 is voted

Citizen win

可以看到这里是狼人(player1)第一晚杀了 player2,是个村民。

然后各自发言,预言家(player0)给 player4 发了个金水。

最后大家各自投票,将 player1 投出,因为 player1 是狼人,所以游戏结束,好人获胜。

后记

优化

如果大家实现完会发现,AI 很呆板,举个例子,我们甚至无法让预言家自由选择何时自曝身份,何时再苟一轮,而这在实际游玩中很重要。

但是大家一定要知道,实现不了不是说 AI 的能力有限,而是我们的能力有限,我们没法给他穷尽这个决策树,我们最多能做到的就是给他一个概率,比如50%去自曝身份,50%去再苟一轮。

而如果想让 AI 更进一步,我们就得进入深度学习的世界(这会放在以后再讲),只有深度学习,才能让 AI 自己通过不断的训练,理解这些变量之间的复杂关系,从而找到最优解。

困惑

最后的话还有我自己的一个困惑,再第一轮中,明明大家对除了被发金水以外的两个人没有任何信息,但却会给他们不同的狼人概率,按理说第一天这两个应该都是0%!!!,应该是弃票的。

下面这六条,第一条是狼人眼中每个人是预言家的概率,第二条是预言家眼中每个人是狼人的概率,第三四六条是每个好人投票时每个人是狼人的概率,第五条是狼人的就不用看了,按理说这里面五条所有的概率都应该是0!!!但是实际结果却不是,这让我感觉非常的困惑。

1
2
3
4
5
6
7
8
9
10
11
INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 0.0), (<__main__.Werewolf object at 0x1035bb160>, 0.0), (<__main__.Citizen object at 0x1035bb2e0>, 0.95), (<__main__.Citizen object at 0x1035bb460>, 0.0), (<__main__.Citizen object at 0x1035bb5e0>, 0.0)]

INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 0.0), (<__main__.Werewolf object at 0x1035bb160>, 0.75), (<__main__.Citizen object at 0x1035bb460>, 0.5), (<__main__.Citizen object at 0x1035bb5e0>, 0.9)]

INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 0.0), (<__main__.Werewolf object at 0x1035bb160>, 0.75), (<__main__.Citizen object at 0x1035bb460>, 0.5), (<__main__.Citizen object at 0x1035bb5e0>, 0.0)]

INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 1.0), (<__main__.Werewolf object at 0x1035bb160>, 0.0), (<__main__.Citizen object at 0x1035bb460>, 0.0), (<__main__.Citizen object at 0x1035bb5e0>, 0.0)]

INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 0.0), (<__main__.Werewolf object at 0x1035bb160>, 1.0), (<__main__.Citizen object at 0x1035bb460>, 0.0), (<__main__.Citizen object at 0x1035bb5e0>, 0.0)]

INFO:__main__:[(<__main__.Prophet object at 0x1035b9f70>, 0.0), (<__main__.Werewolf object at 0x1035bb160>, 0.75), (<__main__.Citizen object at 0x1035bb460>, 0.5), (<__main__.Citizen object at 0x1035bb5e0>, 0.0)]

里面最大的可能是 KB 不完整,也有可能在所有的可能情况下,排除了某些情况后算出来的概率就是这样,不知道有没有概率好的能解释解释。