山丘之王:速度线索AI


24

速度线索

Cluedo / Clue是一款经典的棋盘游戏,具有引人入胜的演绎游戏元素。Speed Clue是3-6位玩家的变体,仅使用纸牌来强调此组件。结果是标准Cluedo和Speed Clue之间的唯一区别是,仍在游戏中的每个玩家都可以在转弯时提出自己喜欢的任何建议,而不必等着掷骰子和其他玩家的建议等待到达特定的房间。如果您以前从未玩过Cluedo,或者想确定两个版本之间的明显区别,可以在此处找到完整的Speed Clue规则集


目标

在2014年5月15日格林威治标准时间00:00之前编写并提交AI程序来玩Speed Clue。在那之后,我将使用所有合法条目进行比赛。AI在比赛中赢得最多比赛的参赛者将赢得挑战。


AI规格

只要AI 通过TCP / IP连接严格使用应用程序协议来与服务器玩游戏,就可以使用几乎任何语言编写您的AI,无论使用哪种技术。有关所有限制的详细说明,请参见此处


怎么玩

首先分叉竞赛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场有效比赛中获胜的百分比。总共有30,000场比赛计入结果,而01失格时约有6,100场比赛被打折。

值得一提的是ray的01AI。我的初步测试表明它是最坚固的,我希望它能赢得竞争。但是,据我所知,它似乎有一个非常间歇的错误,导致它消除了所有可能的解决方案。比赛01的bug被发现后,比赛结束了所有三人比赛,并开始了四人比赛(12,000场比赛!)。如果我仅考虑三人比赛的排名,结果如下所示:

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%

我本来打算对结果进行一些数据挖掘,但是我很累。我在使比赛一直进行(电源故障,系统重新启动)过程中遇到了技术难题,必须完全重写比赛服务器以保存其进行过程中的进度。如果有人仍然感兴趣,我将注释并使用生成的所有结果文件提交对代码的所有更改。如果我决定也进行数据挖掘,那么我的结果也将添加到存储库中。


感谢参与!


4
您可以将服务器的副本提供给参赛者进行测试吗?
彼得·泰勒

you must accept two port numbers: the first will be the port to which your program will listen, and the second will be the port to which your program will send.,为什么要两个端口?
Hasturkun 2014年

1
@PeterTaylor,我将在编写服务器后立即提供该服务器的副本。你为什么觉得我给一个月?;)
sadakatsu 2014年

@Hasturkun,我为服务器计划的体系结构是它将通过命令行启动您的提交。它将选择每个程序用来向其发送消息的端口,以便它可以轻松识别哪个程序(注意,该协议不包含任何标识符)。另外,每个程序都需要知道向哪个端口发送消息,以便服务器可以实际接收消息。这是每个提交必须作为命令行参数接收的两个端口。
sadakatsu 2014年

1
我编写的唯一网络程序使用UDP。我决定使用TCP / IP来(1)理解两者之间的任何区别,以及(2)使用最能支持此操作所需的锁步播放器更新的技术。
sadakatsu 2014年

Answers:


5

AI01-Python 3

我还找不到更好的名称:-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代码可以在这里找到。


您可以通过AI提出请求请求吗?我想将其放入竞赛回购中。
sadakatsu 2014年

@gamecoder我制作了更强大的AI01并发送了请求请求。

1
按照目前的情况,您的01最强。在我进行的试验中,它始终赢得约67%的比赛胜利。我希望我们能在比赛结束前看到一些有挑战性的参赛作品。
sadakatsu 2014年

看看我的SpockAI。对抗效果很好01。我不知道它是否会赢得比赛,但是我很高兴看到您的获胜人数减少了;)
sadakatsu

@gamecoder实际上,我几天前已经根据新规则更新了AI。很高兴看到您的新条目。它的性能似乎不错,但由于效率低下,所以我没有对其进行多次测试。也许您可以使其更快,以便我们更轻松地进行测试。

4

SimpleCluedoPlayer.java

此类使用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);
        }
    }
}

非常干净的代码。我怀疑任何看着测试服务器或随机播放器的人都有这种感觉。我也认为您不能拥有比这更简单的AI ^ _ ^
sadakatsu 2014年

4

SpockAI

识别码: gamecoder-SpockAI

回购条目: 单击此处

已选:

技术:基于Java 7com.sadakatsu.clue.jar

参数: {identifier} portNumber [logOutput: true|false]

描述:

SpockAI是根据Knowledge我编写的一个类构建的Speed Clue播放器。本Knowledge类代表所有可能的状态,游戏可能给到目前为止发生了什么事。它代表游戏的解决方案和玩家的可能手形,并在每次学习某些东西时使用迭代推论来尽可能减少这些组。 SpockAI使用此类确定要确保哪些建议具有最有用的最坏情况结果,并轮流随机选择这些建议之一。当它需要反驳建议时,它会尝试显示已经显示了建议AI的卡片,或者显示它已将可能性最小化的类别中的卡片。它只有在知道解决方案后才提出指控。

我用来确定最佳建议的启发式方法如下。从建议中学到所有信息之后,可能的解决方案和可能的球员手牌将减少(除非该建议没有透露新的信息)。从理论上讲,最好的建议是最大程度地减少可能解决方案的数量的建议。在平局的情况下,我认为最好的建议是减少玩家可用的手数。因此,对于每个建议,我都会尝试所有不会导致知识矛盾的可能结果。建议/解决方案/手计数方面改善最少的结果被认为是建议的结果。然后,我比较所有建议的结果,然后选择哪个建议的结果最好。这样,我保证了最佳的最坏情况下的信息增益。

我正在考虑对可能的解决方案和可能的玩家手牌进行蛮力组合分析,以使其SpockAI变得更强壮,但由于SpockAI这已经是最慢,最耗费资源的入门,因此我可能会跳过。

免责声明:

我原本打算在几周前为这场比赛发布AI。就目前而言,我直到上周五才开始编写我的AI,而且我一直在代码中发现可笑的错误。因此,SpockAI在截止日期之前上班的唯一方法就是使用大型线程池。最终结果是(当前)SpockAI可以达到+ 90%的CPU利用率和2GB +的内存使用率(尽管我为此归咎于垃圾收集器)。我打算参加SpockAI比赛,但如果其他人认为这违反了规则,我将把“优胜者”的头衔授予应SpockAI赢得的第二名。如果您有这种感觉,请对此答案发表评论。


3

InferencePlayer.java

Github上的完整代码(请注意:此代码AbstractCluedoPlayer我之前的代码相同SimpleCluedoPlayer)。

该播放器的真正核心是其PlayerInformation阶级(此处略有修饰):

private static class PlayerInformation {
    final PlayerInformation[] context;
    final int playerId;
    final int handSize;
    Set<Integer> clauses = new HashSet<Integer>();
    Set<Card> knownHand = new HashSet<Card>();
    int possibleCards;
    boolean needsUpdate = false;

    public PlayerInformation(PlayerInformation[] context, int playerId, boolean isMe, int handSize, Set<Card> myHand) {
        this.context = context;
        this.playerId = playerId;
        this.handSize = handSize;
        if (isMe) {
            knownHand.addAll(myHand);
            possibleCards = 0;
            for (Card card : knownHand) {
                int cardMask = idsByCard.get(card);
                clauses.add(cardMask);
                possibleCards |= cardMask;
            }
        }
        else {
            possibleCards = allCardsMask;
            for (Card card : myHand) {
                possibleCards &= ~idsByCard.get(card);
            }

            if (playerId == -1) {
                // Not really a player: this represents knowledge about the solution.
                // The solution contains one of each type of card.
                clauses.add(suspectsMask & possibleCards);
                clauses.add(weaponsMask & possibleCards);
                clauses.add(roomsMask & possibleCards);
            }
        }
    }

    public void hasCard(Card card) {
        if (knownHand.add(card)) {
            // This is new information.
            needsUpdate = true;
            clauses.add(idsByCard.get(card));

            // Inform the other PlayerInformation instances that their player doesn't have the card.
            int mask = idsByCard.get(card);
            for (PlayerInformation pi : context) {
                if (pi != this) pi.excludeMask(mask);
            }

            if (knownHand.size() == handSize) {
                possibleCards = mask(knownHand);
            }
        }
    }

    public void excludeMask(int mask) {
        if (knownHand.size() == handSize) return; // We can't benefit from any new information.

        if ((mask & possibleCards) != 0) {
            // The fact that we have none of the cards in the mask contains some new information.
            needsUpdate = true;
            possibleCards &= ~mask;
        }
    }

    public void disprovedSuggestion(Suggestion suggestion) {
        if (knownHand.size() == handSize) return; // We can't benefit from any new information.

        // Exclude cards which we know the player doesn't have.
        needsUpdate = clauses.add(mask(suggestion.cards()) & possibleCards);
    }

    public void passedSuggestion(Suggestion suggestion) {
        if (knownHand.size() == handSize) return; // We can't benefit from any new information.

        excludeMask(mask(suggestion.cards()));
    }

    public boolean update() {
        if (!needsUpdate) return false;

        needsUpdate = false;

        // Minimise the clauses, step 1: exclude cards which the player definitely doesn't have.
        Set<Integer> newClauses = new HashSet<Integer>();
        for (int clause : clauses) {
            newClauses.add(clause & possibleCards);
        }
        clauses = newClauses;

        if (clauses.contains(0)) throw new IllegalStateException();

        // Minimise the clauses, step 2: where one clause is a superset of another, discard the less specific one.
        Set<Integer> toEliminate = new HashSet<Integer>();
        for (int clause1 : clauses) {
            for (int clause2 : clauses) {
                if (clause1 != clause2 && (clause1 & clause2) == clause1) {
                    toEliminate.add(clause2);
                }
            }
        }
        clauses.removeAll(toEliminate);

        // Every single-card clause is a known card: update knownHand if necessary.
        for (int clause : clauses) {
            if (((clause - 1) & clause) == 0) {
                Card singleCard = cardsById.get(clause);
                hasCard(cardsById.get(clause));
            }
        }

        // Every disjoint set of clauses of size equal to handSize excludes all cards not in the union of that set.
        Set<Integer> disjoint = new HashSet<Integer>(clauses);
        for (int n = 2; n <= handSize; n++) {
            Set<Integer> nextDisjoint = new HashSet<Integer>();
            for (int clause : clauses) {
                for (int set : disjoint) {
                    if ((set & clause) == 0) nextDisjoint.add(set | clause);
                }
            }
            disjoint = nextDisjoint;
        }

        for (int set : disjoint) excludeMask(~set);

        return true;
    }
}

它统一了以下信息:玩家不反对的建议(表明他们不持有任何一张卡),他们反对的建议(表明他们至少持有其中一张卡)以及位置确定的卡。然后,迭代地应用一些基本规则将信息压缩成其实质。

我认为没有更多的确定性信息(通过虚假指控除外,我认为这种情况很少见,尽管我可能忽略了)。还有就是一个更复杂的玩家估计概率该玩家X有卡Y潜力...

可能要承认的重大改进的另一个领域是在决定提出哪个建议。我尝试使用笨重的笨重方法来最大化信息获取,但是在评估从不同的假设证明中获得的知识的相对价值时,有很多理由不充分的启发式方法。但是,在其他人发布有价值的对手之前,我不会尝试调整启发式方法。


我无法运行您的SimpleCluedoPlayer或InferencePlayer。我在“ SpeedClueContest / entries / peter_taylor /”目录中运行并成功生成了JAR。我已经尝试了这些JAR的相对路径和绝对路径,并按顺序将它们传递给“ identifier”和“ portNumber”,但是TestServer挂起,等待它们各自的“ identifier alive”消息。我在寻找并且找不到“ /tmp/speed-cluedo-player”+identifier+“.log”。我是否以某种方式弄乱了流程?
sadakatsu 2014年

@gamecoder,我可能不应该硬编码/tmp。它应该是一个简单的补丁;我会尽快调查。
彼得·泰勒

1
您的修复有效;我已经将其合并到回购中。现在,我开始仔细阅读InferencePlayer,以确保它与我开始研究> ___ <的LogicalAI之间存在足够的差异
sadakatsu 2014年

你的对手来了:-)

@Ray,太好了。我将尝试剖析您的AI,并在某些时候看它与我的AI有何不同:粗略地看一看,它似乎使用了类似的分析。
彼得·泰勒

2

CluePaddle(ClueStick / ClueBat / ClueByFour)-C#

我已经编写了ClueBot,这是一个C#客户端,可以轻松地实现AI以及各种AI,包括最严重的尝试CluePaddle。该代码位于https://github.com/jwg4/SpeedClueContest/tree/clue_paddle,其中有拉取请求开始,将其合并到上游。

ClueStick是一种概念证明,基本上只是猜测,而忽略了大多数发生的事情。ClueBat是另一种愚蠢的AI,只是它试图利用ClueStick的一个缺陷来迫使其进行虚假指控。ClueByFour是一种合理的AI,因为它会提出合理的建议并记住其他人显示的卡片。

CluePaddle是最聪明的。它试图找出谁拥有什么,不仅基于提供了哪些证明,还基于哪些玩家没有提供给定建议的证明。它没有考虑每个玩家拥有atm的卡数,但是这是固定的。它包括几个相当长的类,因此我不会在此处发布整个代码,但是以下方法给出了一种味道。

public void Suggestion(int suggester, MurderSet suggestion, int? disprover, Card disproof)
{
  List<int> nonDisprovers = NonDisprovers(suggester, disprover).ToList();

  foreach (var player in nonDisprovers)
  {
    m_cardTracker.DoesntHaveAnyOf(player, suggestion);
  }

  if (disprover != null && disproof == null)
  {
    // We know who disproved it but not what they showed.
    Debug.Assert(disprover != m_i, "The disprover should see the disproof");
    Debug.Assert(suggester != m_i, "The suggester should see the disproof");
    m_cardTracker.DoesntHaveAllOf(suggester, suggestion);
    m_cardTracker.HasOneOf((int)disprover, suggestion);
  }

  if (disproof != null)
  {
    // We know who disproved it and what they showed.
    Debug.Assert(disprover != null, "disproof is not null but disprover is null");
    m_cardTracker.DoesHave((int)disprover, disproof.Value);
  }
}

如果这4个对手互相对战,则CluePaddle赢得了迄今为止最多的比赛,其中ClueByFour排名第二,另外两个则无处可战。

(到目前为止)只有CluePaddle是竞争产品。用法:

CluePaddle.exe identifier port

如果其他人想制作C#AI,只需在解决方案中创建一个ConsoleApplication项目,IClueAI在一个类中实现该接口,然后Program从中派生ProgramTemplate并复制其他项目的用途Main()。唯一的依赖项是用于单元测试的NUnit,您可以轻松地从代码中删除所有测试(但不要安装NUnit)。


我试图编译您的AI,并在ContestServer中对其进行测试(即将发布)。该CluePaddle项目无法编译,声称NUnit即使其他项目也可以编译,该项目也未安装。那些可编译的代码最终会在测试期间停滞不前,并且Java报告了连接重置错误。您能帮我确定我做错了什么吗?
sadakatsu

校正:ClueStick是我尝试启动时唯一停滞的AI。另外两个参加比赛,最终因同样的违规而被取消比赛资格。 ClueByFour因未持有任何卡片而未能重复提出的未经证实的建议而被取消资格。 ClueBat因提出指控而持有其已被显示或已在手中的卡片而被取消资格。请查看修改后的AI限制以确保合规性。
sadakatsu 2014年

@gamecoder您是否已安装NUnit?如果无法安装,则可以有条件地删除单元测试代码。CluePaddle是我的实际参赛作品-我不太担心其他两个人的违规行为,因为他们并没有真正赢得比赛。
jwg 2014年

我也有一些更新CluePaddle。稍后,我将对这些请求进行拉取。
jwg 2014年

我确实安装了NUnit。我能够使用其他项目的引用在MSVS中探索其名称空间。我期待您的要求。
sadakatsu 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.