キングオブザヒル:Speed Clue AI


24

スピードの手がかり

Cluedo / Clueは、説得力のある控除ゲームプレイコンポーネントを備えたクラシックなボードゲームです。Speed Clueは、カードのみを使用してこのコンポーネントを強調する3〜6プレーヤーのバリエーションです。その結果、標準のCluedoとSpeed Clueの唯一の違いは、サイコロの出目や他のプレイヤーの提案に翻弄されて特定の部屋に到着するのを待つのではなく、ゲーム内にいる各プレイヤーが自分のターンで好きな提案をすることができるということです。以前にCluedoをプレイしたことがない場合、または2つのバージョンの明確な違いを確認したい場合は、ここで完全なSpeed Clueルールセットを見つけることができます


ゴール

2014年5月15日00:00 GMTより前にSpeed ClueをプレイするAIプログラムを作成して送信します。その後、すべての法的エントリを使用してトーナメント開催します。AIがトーナメントで最も多くのゲームに勝った参加者が挑戦に勝ちます。


AI仕様

サーバーでゲームをプレイするためにTCP / IP接続を介してアプリケーションプロトコルを厳密に使用する限り、使用するテクニックを使用して、選択したほぼすべての言語でAIを作成できます。すべての制限の詳細な説明はここで見つけることができます


遊び方

コンテストGitHubリポジトリをフォークすることから始めますentriesStackExchangeユーザー名を使用して命名されたディレクトリの下にディレクトリを追加し、 そのフォルダでコードを開発します。エントリを送信する準備ができたら、リビジョンを使用してプルリクエストを行い、このサイトでエントリを発表するためのこれらの指示に従ってください

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件のマッチが結果にカウントされ、6,100件ほど01が失格となったときに割引されました。

名誉ある言及は、レイの01AI に行く必要があります。私の最初のテストでは、それが最強であることが示され、競争に勝つことが期待されました。ただし、非常に断続的なバグがあり、推測できる限り、考えられるすべての解決策を排除しているようです。トーナメントは3人のプレーヤーの試合をすべて終了し、01のバグが明らかになったときに4人のプレーヤーの試合(12,000ゲームが開始されました!)を開始しました。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%

結果をデータマイニングすることを計画していましたが、疲れ果てています。コンテストを最後までやり遂げるのに技術的な困難があり(停電、システムの再起動)、進行中の進捗を保存するためにコンテストサーバーを完全に書き直す必要がありました。誰かがまだ興味がある場合に備えて、生成されたすべての結果ファイルを使用して、コードに対するすべての変更をコメントし、コミットします。データマイニングも実行することにした場合、結果もリポジトリに追加されます。


遊んでくれてありがとう!


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.、なぜ2つのポートですか?
ハスタークン

1
@PeterTaylor、サーバーのコピーを作成したらすぐに利用できるようにします。なぜ私が月を与えていると思いますか?;)
貞勝14

@Hasturkun、私がサーバー用に計画したアーキテクチャは、コマンドラインを介して送信を開始することです。どのプログラムがどのプログラムであるかを簡単に識別できるように、各プログラムがメッセージの送信に使用するポートを選択します(プロトコルには識別子が含まれないことに注意してください)。さらに、各プログラムは、サーバーが実際にメッセージを受信できるように、メッセージを送信するポートを認識する必要があります。これらは、各サブミッションがコマンドライン引数として受け取る必要がある2つのポートです。
貞勝14

1
私が書いた唯一のネットワークプログラムはUDPを使用しています。TCP / IPを使用して、(1)2つの違いを理解し、(2)これが機能するために必要なロックステッププレーヤーの更新を最適にサポートするテクノロジーを使用することにしました。
貞勝

回答:


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でプルリクエストを行うことはできますか?コンテストリポジトリに登録したいと思います。
貞勝

@gamecoderより強力なAI01を作成し、プルリクエストを送信しました。
レイ

1
現状では、01が最強です。私が実行したトライアルでは、競合する試合の約67%を一貫して獲得しています。コンテストが終了する前に、チャレンジできるような堅実なエントリーが見られることを期待しています。
貞勝

私のをチェックしてくださいSpockAI。に対してかなりよく機能し01ます。それが競争に勝つかどうかはわかりませんが、あなたの勝利数が減少するのを見てうれしいです。)
貞勝

@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を使用することはできないと思います^ _ ^
貞勝14

4

SpockAI

識別子: gamecoder-SpockAI

リポジトリエントリ: ここをクリック

選択済み:はい

テクノロジー: Java 7ベースcom.sadakatsu.clue.jar

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

説明:

SpockAIKnowledge私が書いたと呼ばれるクラスの上に構築されたスピードの手掛かりプレーヤーです。のKnowledgeクラスは、これまでに何が起こったのか、ゲームが与えられている可能性があること、すべての可能な状態を表しています。ゲームのソリューションとプレイヤーの可能なハンドをセットとして表し、反復演ductionを使用して、何かが学習されるたびにこれらのセットを可能な限り削減します。 SpockAIこのクラスを使用して、最も役立つ最悪の結果が得られることが確実な提案を決定し、その提案の1つをランダムに選択します。提案に反論する必要がある場合、すでに提案しているAIを示​​したカードを表示するか、可能性を最小限に抑えたカテゴリのカードを表示しようとします。解決策を知っている場合にのみ告発します。

最良の提案を決定するために使用したヒューリスティックは次のとおりです。すべての情報が提案から学習された後、可能な解決策および可能なプレーヤーのハンドセットは削減されます(提案が新しい情報を明らかにしない限り)。理論的には、最良の提案は可能な解決策の数を最も減らすものです。同点の場合、プレイヤーの可能なハンドの数を最も減らす提案がより良いと思います。したがって、提案ごとに、知識の矛盾につながらないすべての可能な結果を​​試します。ソリューション/ハンドカウントの改善が最も少ない結果が、提案の結果と見なされます。次に、すべての提案の結果を比較し、どれが最良の結果をもたらすかを選択します。このようにして、最適な最悪の場合の情報利得を保証します。

考えられる解決策と考えられるプレイヤーの手札のブルートフォースの組み合わせ分析を追加してSpockAIさらに強くすることを検討していますが、SpockAIすでに最も遅く、最もリソースを消費するエントリなので、おそらくスキップします。

免責事項:

私は数週間前にこのコンテストのAIをリリースするつもりでした。現状では、先週の金曜日までAIの作成を開始できませんでした。また、コードにとんでもないバグを見つけ続けました。このためSpockAI、締め切り前に仕事を始めることができた唯一の方法は、大きなスレッドプールを使用することでした。最終的な結果は、(現在)SpockAIがCPU使用率+ 90%と2GB +のメモリ使用量に達する可能性があることです(ただし、これはガベージコレクターのせいです)。私SpockAIはコンテストに出場するつもりですが、他の人がそれが規則に違反していると感じた場合、私は「勝者」のタイトルを2位に授与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;
    }
}

プレーヤーが反証しなかった提案(それらのカードを保持していないことを示す)、反証した提案(それらのカードの少なくとも1つを保持していることを示す)、および場所が特定のカードに関する情報を統合します。次に、いくつかの基本的なルールを繰り返し適用して、その情報を本質に圧縮します。

私は何かを見落としているかもしれませんが、これ以上の決定論的な情報は得られないと思います(私は気にすることはあまりにもまれであると仮定している誤った告発によるものを除きます)。ありますプレイヤーXがカードYを持っていることを推定確率をより洗練された選手の可能性が...

おそらく大幅な改善を認めるもう1つの分野は、どの提案を行うかを決定することです。私はかなり不格好で重い力のアプローチを使用して情報ゲインを最大化しようとしますが、さまざまな仮説的反証から得られた知識の相対的なメリットを評価する際に、不十分に正当化されたヒューリスティックがたくさんあります。ただし、他の誰かが立派な相手を投稿するまで、ヒューリスティックの調整を試みるつもりはありません。


SimpleCluedoPlayerまたはInferencePlayerを実行できません。「SpeedClueContest / entries / peter_taylor /」ディレクトリでantを実行し、JARを正常に生成しました。これらのJARへの相対パスと絶対パスを試し、「identifier」と「portNumber」の順に渡しましたが、TestServerはそれぞれの「identifier alive」メッセージを待ってハングします。「/tmp/speed-cluedo-player」+identifier+".log "を探しましたが見つかりません。何とかプロセスを台無しにしましたか?
貞勝14

@gamecoder、おそらくハードコーディングすべきではありません/tmp。単純なパッチである必要があります。間もなく調べます。
ピーターテイラー14

1
修正は機能します。リポジトリにマージしました。今、私はInferencePlayerを注意深く読んで、それと私が取り組んでいたLogicalAIとの間に十分な違いがあることを確認します> ___ <
sadakatsu 14

ここにあなたの対戦相手が来ます:-)
レイ14

@Ray、すばらしい。私はあなたのAIを分析し、ある時点で私のAIとどのように異なるかを確認します:大まかに一見すると、同様の分析を使用しているようです。
ピーターテイラー14

2

CluePaddle(ClueStick / ClueBat / ClueByFour)-C#

ClueBot、AIの実装が簡単なC#クライアント、およびCluePaddleと呼ばれる最も深刻な試みを含むさまざまなAIを作成しました。コードはhttps://github.com/jwg4/SpeedClueContest/tree/clue_paddleにありますあり、プルリクエストがアップストリームへのマージを開始しています。

ClueStickは、基本的には何が起こるかを推測して無視するだけの概念実証です。ClueBatはもう1つの愚かな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が2番目で、他の2つはどこにも勝ちません。

CluePaddleのみが(今のところ)競争力のあるエントリーです。使用法:

CluePaddle.exe identifier port

他の誰かがC#AIを作成する場合は、ソリューションでConsoleApplicationプロジェクトを作成IClueAIし、クラスにインターフェイスを実装し、他のプロジェクトのProgram派生物ProgramTemplateを作成してコピーしMain()ます。ユニットテストの唯一の依存関係はNUnitであり、コードからすべてのテストを簡単に削除できます(ただし、NUnitをインストールしないでください)。


私はあなたのAIをコンパイルし、ContestServerでテストしようとしました(すぐに投稿されます)。CluePaddleこのプロジェクトは、その主張し、コンパイルされませんNUnit他のプロジェクトがコンパイルを行うにもかかわらず、インストールされていません。コンパイルしたものはテスト中に停止し、Javaは接続リセットエラーを報告します。私が何か間違ったことをしたかどうかを判断するのを手伝ってもらえますか?
貞勝

修正:ClueStick起動しようとすると失速する唯一のAIです。他の2人はトライアルトーナメントに出場し、最終的に同じ違反で失格となります。 ClueByFourカードを保持していないときに告発として行った、不当な提案を繰り返さないことで失格となります。 ClueBat見せられた、または手元にあるカードを持っているとの告発で失格となります。コンプライアンスを確実にするために、改訂されたAI制限をチェックしください。
貞勝

@gamecoder NUnitがインストールされていますか?インストールできない場合は、単体テストコードを条件付きで削除できます。CluePaddleは私の実際のエントリーです。他の2人が実際に勝つためにプレーしていないので、他の2人による違反についてはあまり心配していません。
JWG

私もいくつかの更新がありますCluePaddle。これらのプル要求は後で行います。
JWG

NUnitをインストールしました。他のプロジェクトの参照を使用して、MSVSで名前空間を探索することができました。プルリクエストをお待ちしております。
貞勝
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.