克雷多/线索是一个经典的棋盘游戏,有一个引人注目的演绎游戏组件。速度线索是一个3-6玩家变体,强调这个组成部分,只使用卡。结果是,标准克鲁多和速度线索之间唯一的区别是,游戏中的每一个玩家都可以提出自己喜欢的任何建议,而不是在骰子和其他玩家的建议的摆布下到达一个特定的房间。如果您以前从未玩过集群,或者希望确定这两个版本之间的显式差异,您可能会找到一个完全速度线索规则集。
编写并提交一个AI程序,在2014年5月15日格林尼治时间00:00之前播放速度线索。之后,我将使用所有合法条目运行锦标赛。在锦标赛中,AI赢得最多比赛的参赛者赢得了挑战。
您可以使用您所选择的任何语言编写AI,只要它通过TCP/IP连接严格使用应用协议来与服务器玩游戏,就可以使用任何技术。有关所有限制的详细解释可以在这里中找到。
从分叉竞赛GitHub存储库开始。在entries
目录下添加一个使用StackExchange用户名命名的目录,并在该文件夹中开发代码。当你准备提交你的条目,提出一个拉请求与你的修订,然后跟随这些指示宣布你的条目在这个网站。
我在core
目录中提供了一些代码和JAR来帮助您入门;有关材料的粗略指南,请参阅我的网站。此外,其他玩家除了他们的条目外,还会提交助手代码,以帮助您起立和运行。花点时间去探索这些条目,在提交之前不要忘记用别人的条目来测试你的条目!
Place | User | AI | Result
------+--------------+--------------------+-------------------------------------------------------
1 | gamecoder | SpockAI | 55.75%
2 | Peter Taylor | InferencePlayer | 33.06%
3 | jwg | CluePaddle | 20.19%
4 | Peter Taylor | SimpleCluedoPlayer | 8.34%
5 | gamecoder | RandomPlayer | 1.71%
---- | ray | 01 | Player "ray-01" [3] sent an invalid accuse message: ""
上面的结果显示,每个合格的AI在它参加的25,200场有效比赛中获胜的百分比。结果显示,总共有3万场比赛,而当01
被取消资格时,大约有6100场比赛被打折。
一个荣誉的提名需要去雷的01
人工智能。我的初步测试显示它是最强的,我期望它能赢得比赛。然而,它似乎有一个非常断断续续的错误,据我所能猜测,导致它消除了所有可能的解决方案。比赛结束了所有的三人比赛,并开始了四人比赛(12,000场比赛!)当01
的S错误被揭露时,如果我只考虑3人的比赛排名,结果如下:
Place | User | AI | Result
------+--------------+--------------------+--------
1 | ray | 01 | 72.10%
2 | gamecoder | SpockAI | 51.28%
3 | Peter Taylor | InferencePlayer | 39.97%
4 | Peter Taylor | SimpleCluedoPlayer | 17.65%
5 | jwg | CluePaddle | 16.92%
6 | gamecoder | RandomPlayer | 2.08%
我本来打算对结果进行一些数据挖掘,但我已经筋疲力尽了。我遇到了技术上的困难,让竞争一路运行(电力故障,系统重新启动),这就必须完全重写竞赛服务器,以节省它的进展,因为它进行。我将注释并提交对代码的所有更改以及生成的所有结果文件,以防任何人仍然感兴趣。如果我也决定进行数据挖掘,我的结果也将被添加到存储库中。
发布于 2014-05-14 16:46:54
标识符:gamecoder-SpockAI
回购条目:单击此处
选定:是
技术:基于com.sadakatsu.clue.jar
的Java 7
论据:{identifier} portNumber [logOutput: true|false]
SpockAI
是一个速度提示播放器,它建立在我编写的一个名为Knowledge
的类之上。Knowledge
类表示游戏可能给出到目前为止发生的一切可能的状态。它代表游戏的解决方案和玩家可能的手作为集合,并使用迭代演绎尽可能减少这些集,每一次学习。SpockAI
使用这个类来确定哪些建议可以得到最有用的最坏结果,并随机选择其中的一个建议。当它需要反驳一个建议时,它试图显示一张它已经显示了建议AI的卡片,或者向它展示一张从它减少可能性最小的类别中得到的卡片。只有当它知道解决办法时,它才会提出指控。
我用来确定最佳建议的启发式方法如下。在从建议中了解到所有信息之后,可能的解决方案和可能的玩家手持设备就会减少(除非建议没有显示新的信息)。理论上,最好的建议是最大限度地减少可能的解决方案的数量。在平局的情况下,我认为建议尽量减少球员可能的手数是更好的。因此,对于每一个建议,我尝试每一个不导致知识矛盾的可能结果。无论哪种结果在解决方案/手数方面改善最少,都被认为是建议的结果。然后,我比较所有建议的结果,并选择哪一个有最好的结果。这样,我保证了最优最坏情况下的信息增益。
我正在考虑添加一个蛮力组合分析可能的解决方案和可能的玩家手,以使SpockAI
更加强大,但由于SpockAI
已经是最慢,最资源密集的入门,我可能会跳过这一点。
几周前我就打算为这次比赛发布一个人工智能。就目前情况而言,我直到上个星期五才开始写我的人工智能,我一直在我的代码中发现荒谬的错误。正因为如此,我能够让SpockAI
在截止日期之前工作的唯一方法就是使用一个大型线程池。最终的结果是,(目前) SpockAI可以达到+90%的CPU利用率和2GB+内存使用率(尽管我将此归咎于垃圾收集器)。我打算在比赛中运行SpockAI
,但是如果其他人觉得这违反了规则,如果SpockAI
赢了,我会把“胜利者”的头衔授予第二名。如果你这样想,请在这个答案上留下一个大意的评论。
发布于 2014-04-27 18:46:00
我还找不到更好的名字:-P。
标识符: ray-ai01
技术: Python 3
选定:是
论据:ai01.py identifier port
描述:通过推理工作。当不知道所有者的卡数小于一个阈值时,这个AI开始通过递归全局推理消除所有不可能的解决方案。否则,它使用局部推理。
#!/usr/bin/env python
import itertools
from speedclue.playerproxy import Player, main
from speedclue.cards import CARDS
from speedclue.protocol import BufMessager
# import crash_on_ipy
class Card:
def __init__(self, name, type):
self.name = name
self.possible_owners = []
self.owner = None
self.in_solution = False
self.disproved_to = set()
self.type = type
def __repr__(self):
return self.name
def log(self, *args, **kwargs):
pass
def set_owner(self, owner):
assert self.owner is None
assert self in owner.may_have
for player in self.possible_owners:
player.may_have.remove(self)
self.possible_owners.clear()
self.owner = owner
owner.must_have.add(self)
self.type.rest_count -= 1
def set_as_solution(self):
# import pdb; pdb.set_trace()
assert self.owner is None
self.type.solution = self
self.in_solution = True
for player in self.possible_owners:
player.may_have.remove(self)
self.possible_owners.clear()
self.type.rest_count -= 1
def __hash__(self):
return hash(self.name)
class CardType:
def __init__(self, type_id):
self.type_id = type_id
self.cards = [Card(name, self) for name in CARDS[type_id]]
self.rest_count = len(self.cards)
self.solution = None
class PlayerInfo:
def __init__(self, id):
self.id = id
self.must_have = set()
self.may_have = set()
self.selection_groups = []
self.n_cards = None
def __hash__(self):
return hash(self.id)
def set_have_not_card(self, card):
if card in self.may_have:
self.may_have.remove(card)
card.possible_owners.remove(self)
def log(self, *args, **kwargs):
pass
def update(self):
static = False
updated = False
while not static:
static = True
if len(self.must_have) == self.n_cards:
if not self.may_have:
break
for card in self.may_have:
card.possible_owners.remove(self)
self.may_have.clear()
static = False
updated = True
if len(self.must_have) + len(self.may_have) == self.n_cards:
static = False
updated = True
for card in list(self.may_have):
card.set_owner(self)
new_groups = []
for group in self.selection_groups:
group1 = []
for card in group:
if card in self.must_have:
break
if card in self.may_have:
group1.append(card)
else:
if len(group1) == 1:
group1[0].set_owner(self)
updated = True
static = False
elif group1:
new_groups.append(group1)
self.selection_groups = new_groups
if len(self.must_have) + 1 == self.n_cards:
# There is only one card remain to be unknown, so this card must
# be in all selection groups
cards = self.may_have.copy()
for group in self.selection_groups:
if self.must_have.isdisjoint(group):
cards.intersection_update(group)
for card in self.may_have - cards:
static = False
updated = True
self.set_have_not_card(card)
# assert self.must_have.isdisjoint(self.may_have)
# assert len(self.must_have | self.may_have) >= self.n_cards
return updated
class Suggestion:
def __init__(self, player, cards, dplayer, dcard):
self.player = player
self.cards = cards
self.dplayer = dplayer
self.dcard = dcard
self.disproved = dplayer is not None
class AI01(Player):
def prepare(self):
self.set_verbosity(0)
def reset(self, player_count, player_id, card_names):
self.log('reset', 'id=', player_id, card_names)
self.fail_count = 0
self.suggest_count = 0
self.card_types = [CardType(i) for i in range(len(CARDS))]
self.cards = list(itertools.chain(*(ct.cards for ct in self.card_types)))
for card in self.cards:
card.log = self.log
self.card_map = {card.name: card for card in self.cards}
self.owned_cards = [self.card_map[name] for name in card_names]
self.players = [PlayerInfo(i) for i in range(player_count)]
for player in self.players:
player.log = self.log
self.player = self.players[player_id]
for card in self.cards:
card.possible_owners = list(self.players)
n_avail_cards = len(self.cards) - len(CARDS)
for player in self.players:
player.may_have = set(self.cards)
player.n_cards = n_avail_cards // player_count \
+ (player.id < n_avail_cards % player_count)
for card in self.owned_cards:
card.set_owner(self.player)
for card in self.cards:
if card not in self.owned_cards:
self.player.set_have_not_card(card)
self.suggestions = []
self.avail_suggestions = set(itertools.product(*CARDS))
self.possible_solutions = {
tuple(self.get_cards_by_names(cards)): 1
for cards in self.avail_suggestions
}
self.filter_solutions()
def filter_solutions(self):
new_solutions = {}
# assert self.possible_solutions
join = next(iter(self.possible_solutions))
for sol in self.possible_solutions:
for card, type in zip(sol, self.card_types):
if card.owner or type.solution and card is not type.solution:
# This candidate can not be a solution because it has a
# card that has owner or this type is solved.
break
else:
count = self.check_solution(sol)
if count:
new_solutions[sol] = count
join = tuple(((x is y) and x) for x, y in zip(join, sol))
self.possible_solutions = new_solutions
updated = False
for card in join:
if card and not card.in_solution:
card.set_as_solution()
updated = True
self.log('found new target', card, 'in', join)
# self.dump()
return updated
def check_solution(self, solution):
"""
This must be called after each player is updated.
"""
players = self.players
avail_cards = set(card for card in self.cards if card.possible_owners)
avail_cards -= set(solution)
if len(avail_cards) >= 10:
return 1
count = 0
def resolve_player(i, avail_cards):
nonlocal count
if i == len(players):
count += 1
return
player = players[i]
n_take = player.n_cards - len(player.must_have)
cards = avail_cards & player.may_have
for choice in map(set, itertools.combinations(cards, n_take)):
player_cards = player.must_have | choice
for group in player.selection_groups:
if player_cards.isdisjoint(group):
# Invalid choice
break
else:
resolve_player(i + 1, avail_cards - choice)
resolve_player(0, avail_cards)
return count
def suggest1(self):
choices = []
for type in self.card_types:
choices.append([])
if type.solution:
choices[-1].extend(self.player.must_have & set(type.cards))
else:
choices[-1].extend(sorted(
(card for card in type.cards if card.owner is None),
key=lambda card: len(card.possible_owners)))
for sgi in sorted(itertools.product(*map(lambda x:range(len(x)), choices)),
key=sum):
sg = tuple(choices[i][j].name for i, j in enumerate(sgi))
if sg in self.avail_suggestions:
self.avail_suggestions.remove(sg)
break
else:
sg = self.avail_suggestions.pop()
self.fail_count += 1
self.log('fail')
self.suggest_count += 1
return sg
def suggest(self):
sg = []
for type in self.card_types:
card = min((card for card in type.cards if card.owner is None),
key=lambda card: len(card.possible_owners))
sg.append(card.name)
sg = tuple(sg)
if sg not in self.avail_suggestions:
sg = self.avail_suggestions.pop()
else:
self.avail_suggestions.remove(sg)
return sg
def suggestion(self, player_id, cards, disprove_player_id=None, card=None):
sg = Suggestion(
self.players[player_id],
self.get_cards_by_names(cards),
self.players[disprove_player_id] if disprove_player_id is not None else None,
self.card_map[card] if card else None,
)
self.suggestions.append(sg)
# Iter through the non-disproving players and update their may_have
end_id = sg.dplayer.id if sg.disproved else sg.player.id
for player in self.iter_players(sg.player.id + 1, end_id):
if player is self.player:
continue
for card in sg.cards:
player.set_have_not_card(card)
if sg.disproved:
# The disproving player has sg.dcard
if sg.dcard:
if sg.dcard.owner is None:
sg.dcard.set_owner(sg.dplayer)
else:
# Add a selection group to the disproving player
sg.dplayer.selection_groups.append(sg.cards)
self.possible_solutions.pop(tuple(sg.cards), None)
self.update()
def update(self):
static = False
while not static:
static = True
for card in self.cards:
if card.owner is not None or card.in_solution:
continue
if len(card.possible_owners) == 0 and card.type.solution is None:
# In solution
card.set_as_solution()
static = False
for type in self.card_types:
if type.solution is not None:
continue
if type.rest_count == 1:
card = next(card for card in type.cards if card.owner is None)
card.set_as_solution()
static = False
for player in self.players:
if player is self.player:
continue
if player.update():
static = False
if self.filter_solutions():
static = False
def iter_players(self, start_id, end_id):
n = len(self.players)
for i in range(start_id, start_id + n):
if i % n == end_id:
break
yield self.players[i % n]
def accuse(self):
if all(type.solution for type in self.card_types):
return [type.solution.name for type in self.card_types]
possible_solutions = self.possible_solutions
if len(possible_solutions) == 1:
return next(possible_solutions.values())
# most_possible = max(self.possible_solutions, key=self.possible_solutions.get)
# total = sum(self.possible_solutions.values())
# # self.log('rate:', self.possible_solutions[most_possible] / total)
# if self.possible_solutions[most_possible] > 0.7 * total:
# self.log('guess', most_possible)
# return [card.name for card in most_possible]
return None
def disprove(self, suggest_player_id, cards):
cards = self.get_cards_by_names(cards)
sg_player = self.players[suggest_player_id]
cards = [card for card in cards if card in self.owned_cards]
for card in cards:
if sg_player in card.disproved_to:
return card.name
return max(cards, key=lambda c: len(c.disproved_to)).name
def accusation(self, player_id, cards, is_win):
if not is_win:
cards = tuple(self.get_cards_by_names(cards))
self.possible_solutions.pop(cards, None)
# player = self.players[player_id]
# for card in cards:
# player.set_have_not_card(card)
# player.update()
else:
self.log('fail rate:', self.fail_count / (1e-8 + self.suggest_count))
self.log('fail count:', self.fail_count, 'suggest count:', self.suggest_count)
def get_cards_by_names(self, names):
return [self.card_map[name] for name in names]
def dump(self):
self.log()
for player in self.players:
self.log('player:', player.id, player.n_cards,
sorted(player.must_have, key=lambda x: x.name),
sorted(player.may_have, key=lambda x: x.name),
'\n ',
player.selection_groups)
self.log('current:', [type.solution for type in self.card_types])
self.log('possible_solutions:', len(self.possible_solutions))
for sol, count in self.possible_solutions.items():
self.log(' ', sol, count)
self.log('id|', end='')
def end():
return ' | ' if card.name in [g[-1] for g in CARDS] else '|'
for card in self.cards:
self.log(card.name, end=end())
self.log()
for player in self.players:
self.log(' *'[player.id == self.player.id] + str(player.id), end='|')
for card in self.cards:
self.log(
' ' + 'xo'[player in card.possible_owners or player is card.owner],
end=end())
self.log()
if __name__ == '__main__':
main(AI01, BufMessager)
AI代码可以找到这里。
发布于 2014-04-18 10:45:41
这个类使用AbstractCluedoPlayer
,它处理所有I/O,并允许逻辑使用一个简单的类型化接口。整件事都是论github。
这以很高的概率击败了随机玩家(在最坏的情况下,它需要15条建议,而随机玩家平均需要162条),但它很容易被打败。我提供它是为了让球滚开。
package org.cheddarmonk.cluedoai;
import java.io.IOException;
import java.io.PrintStream;
import java.net.UnknownHostException;
import java.util.*;
/**
* A simple player which doesn't try to make inferences from partial information.
* It merely tries to maximise the information gain by always making suggestions involving cards which
* it does not know to be possessed by a player, and to minimise information leakage by recording who
* has seen which of its own cards.
*/
public class SimpleCluedoPlayer extends AbstractCluedoPlayer {
private Map<CardType, Set<Card>> unseenCards;
private Map<Card, Integer> shownBitmask;
private Random rnd = new Random();
public SimpleCluedoPlayer(String identifier, int serverPort) throws UnknownHostException, IOException {
super(identifier, serverPort);
}
@Override
protected void handleReset() {
unseenCards = new HashMap<CardType, Set<Card>>();
for (Map.Entry<CardType, Set<Card>> e : Card.byType.entrySet()) {
unseenCards.put(e.getKey(), new HashSet<Card>(e.getValue()));
}
shownBitmask = new HashMap<Card, Integer>();
for (Card myCard : myHand()) {
shownBitmask.put(myCard, 0);
unseenCards.get(myCard.type).remove(myCard);
}
}
@Override
protected Suggestion makeSuggestion() {
return new Suggestion(
selectRandomUnseen(CardType.SUSPECT),
selectRandomUnseen(CardType.WEAPON),
selectRandomUnseen(CardType.ROOM));
}
private Card selectRandomUnseen(CardType type) {
Set<Card> candidates = unseenCards.get(type);
Iterator<Card> it = candidates.iterator();
for (int idx = rnd.nextInt(candidates.size()); idx > 0; idx--) {
it.next();
}
return it.next();
}
@Override
protected Card disproveSuggestion(int suggestingPlayerIndex, Suggestion suggestion) {
Card[] byNumShown = new Card[playerCount()];
Set<Card> hand = myHand();
int bit = 1 << suggestingPlayerIndex;
for (Card candidate : suggestion.cards()) {
if (!hand.contains(candidate)) continue;
int bitmask = shownBitmask.get(candidate);
if ((bitmask & bit) == bit) return candidate;
byNumShown[Integer.bitCount(bitmask)] = candidate;
}
for (int i = byNumShown.length - 1; i >= 0; i--) {
if (byNumShown[i] != null) return byNumShown[i];
}
throw new IllegalStateException("Unreachable");
}
@Override
protected void handleSuggestionResponse(Suggestion suggestion, int disprovingPlayerIndex, Card shown) {
if (shown != null) unseenCards.get(shown.type).remove(shown);
else {
// This player never makes a suggestion with cards from its own hand, so we're ready to accuse.
unseenCards.put(CardType.SUSPECT, Collections.singleton(suggestion.suspect));
unseenCards.put(CardType.WEAPON, Collections.singleton(suggestion.weapon));
unseenCards.put(CardType.ROOM, Collections.singleton(suggestion.room));
}
}
@Override
protected void recordSuggestionResponse(int suggestingPlayerIndex, Suggestion suggestion, Card shown) {
shownBitmask.put(shown, shownBitmask.get(shown) | (1 << suggestingPlayerIndex));
}
@Override
protected void recordSuggestionResponse(int suggestingPlayerIndex, Suggestion suggestion, int disprovingPlayerIndex) {
// Do nothing.
}
@Override
protected Suggestion makeAccusation() {
Set<Card> suspects = unseenCards.get(CardType.SUSPECT);
Set<Card> weapons = unseenCards.get(CardType.WEAPON);
Set<Card> rooms = unseenCards.get(CardType.ROOM);
if (suspects.size() * weapons.size() * rooms.size() == 1) {
return new Suggestion(suspects.iterator().next(), weapons.iterator().next(), rooms.iterator().next());
}
return null;
}
@Override
protected void recordAccusation(int accusingPlayer, Suggestion accusation, boolean correct) {
// Do nothing.
}
//*********************** Public Static Interface ************************//
public static void main(String[] args) throws Exception {
try {
System.setOut(new PrintStream("/tmp/speed-cluedo-player" + args[0]+".log"));
new SimpleCluedoPlayer(args[0], Integer.parseInt(args[1])).run();
} catch (Throwable th) {
th.printStackTrace(System.out);
}
}
}
https://codegolf.stackexchange.com/questions/25793
复制相似问题