언덕의 왕 : 스피드 단서 AI


24

스피드 단서

Cluedo / Clue 는 매력적인 추론 게임 플레이 구성 요소가 포함 된 클래식 보드 게임입니다. Speed ​​Clue는 카드 만 사용하여이 구성 요소를 강조하는 3-6 플레이어 변형입니다. 그 결과 표준 Cluedo와 Speed ​​Clue의 유일한 차이점은 게임에 남아있는 각 플레이어가 주사위 굴림과 다른 플레이어의 제안에 따라 특정 방에 도달하기를 기다리는 대신 자신의 차례에 원하는 제안을 할 수 있다는 것입니다. 이전에 Cluedo를 사용한 적이 없거나 두 버전 간의 명백한 차이점을 확인하려면 여기 에서 전체 Speed ​​Clue 규칙을 찾을 수 있습니다 .


2014 년 5 월 15 일 00:00 GMT 전에 Speed ​​Clue를 재생하기위한 AI 프로그램을 작성하여 제출하십시오. 그 후, 나는 모든 법적 출전을 사용하여 토너먼트 를 진행할 것 입니다. 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.왜 두 개의 포트?
Hasturkun

1
@PeterTaylor, 서버를 쓰 자마자 사용 가능한 서버 사본을 만들 것입니다. 내가 한 달을주고 있다고 생각하는 이유는 무엇입니까? ;)
sadakatsu

@Hasturkun, 서버를 위해 계획 한 아키텍처는 명령 줄을 통해 제출을 시작한다는 것입니다. 어떤 프로그램이 어떤 프로그램인지 쉽게 식별 할 수 있도록 각 프로그램이 메시지를 보내는 데 사용할 포트를 선택합니다 (프로토콜에는 식별자가 포함되지 않음). 또한 각 프로그램은 서버가 실제로 메시지를 수신 할 수 있도록 메시지를 보낼 포트를 알아야합니다. 이들은 각 제출이 명령 행 인수로 수신해야하는 두 개의 포트입니다.
sadakatsu

1
내가 작성한 유일한 네트워킹 프로그램은 UDP를 사용합니다. 나는 TCP / IP를 사용하여 (1) 둘 사이의 차이점을 이해하고 (2) 이것이 작동하는 데 필요한 잠금 단계 플레이어 업데이트를 가장 잘 지원하는 기술을 사용하기로 결정했습니다.
sadakatsu

답변:


5

AI01-파이썬 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

@ gamecoder 더 강력한 AI01을 만들고 풀 요청을 보냈습니다.
Ray

1
현재 상황은 01이 가장 강합니다. 내가 실시한 시련에서, 그것은 경쟁하는 경기의 ~ 67 %를 지속적으로 이깁니다. 컨테스트가 끝나기 전에 도전 할 수있는 확실한 내용이 표시되기를 바랍니다.
sadakatsu

내 확인하십시오 SpockAI. 그것은 꽤 잘 수행합니다 01. 나는 그것이 경쟁에서 이길 지 모르겠다. 그러나 나는 당신의 승리 카운트가 감소한 것을 보게되어 기쁘다; )
sadakatsu

@gamecoder 사실, 나는 새로운 규칙에 따라 며칠 전에 AI를 업데이트했습니다. 새 항목을 보게되어 기쁩니다. 성능이 좋지만 비효율적이므로 여러 번 테스트하지 않았습니다. 테스트하기 쉽도록 더 빠르게 만들 수 있습니다.
Ray

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

4

SpockAI

식별자 : gamecoder-SpockAI

저장소 항목 : 여기를 클릭하십시오

선택 :

기술 : Java 7 기반com.sadakatsu.clue.jar

인수 : {identifier} portNumber [logOutput: true|false]

기술:

SpockAIKnowledge내가 쓴 클래스에 구축 된 스피드 단서 플레이어 입니다. 이 Knowledge클래스는 게임이 지금까지 일어난 일을 줄 수 있었던 모든 가능한 상태를 나타냅니다. 게임 솔루션과 플레이어의 가능한 손을 세트로 나타내고 반복적 인 추론을 사용하여 무언가를 배울 때마다 가능한 한 이러한 세트를 줄입니다. SpockAI이 클래스를 사용하여 가장 유용한 최악의 결과를 얻을 수있는 제안을 결정하고 해당 제안 중 하나를 임의로 선택합니다. 제안을 반증해야하는 경우, 제안 된 AI를 이미 표시 한 카드를 표시하거나 가능성이 가장 낮은 범주의 카드를 표시하려고합니다. 솔루션을 알고있을 때만 고발합니다.

최상의 제안을 결정하는 데 사용한 휴리스틱은 다음과 같습니다. 제안에서 모든 정보를 얻은 후에는 제안에 새로운 정보가 표시되지 않는 한 가능한 솔루션 및 가능한 플레이어 핸드 세트가 줄어 듭니다. 이론적으로 가장 좋은 제안은 가능한 솔루션 수를 가장 줄이는 제안입니다. 동점 인 경우, 플레이어의 가능한 핸드 수를 가장 줄이는 제안이 더 좋다고 가정합니다. 따라서 각 제안에 대해 지식 모순으로 이어지지 않는 가능한 모든 결과를 시도합니다. 솔루션 / 핸드 카운트에서 가장 적은 개선 결과는 제안으로 얻을 수있는 결과로 간주됩니다. 그런 다음 모든 제안의 결과를 비교하고 가장 적합한 결과를 선택합니다. 이런 식으로, 나는 최적의 최악의 정보 획득을 보장합니다.

가능한 솔루션과 가능한 플레이어 손에 대한 무차별 대입 조합 분석을 추가하여 SpockAI더 강하게 만들 것을 고려하고 있지만 SpockAI이미 가장 느리고 자원 집약적 인 항목이므로 건너 뛸 것입니다.

기권:

몇 주 전에이 콘테스트의 AI를 출시하려고했습니다. 지난 주 금요일까지 AI 작성을 시작할 수 없었고 코드에서 우스운 버그를 계속 발견했습니다. 이 때문에 SpockAI마감일 이전에 일을 할 수 있었던 유일한 방법 은 큰 스레드 풀을 사용하는 것입니다. 결과적으로 (현재) SpockAI는 + 90 % CPU 사용률과 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;
    }
}

그것은 플레이어가 반증하지 않은 제안들 (그 카드들 중 어느 것도 가지고 있지 않다는 것을 나타냄), 그들이 반박 한 제안들 (적어도 그 카드들 중 하나 이상을 가지고 있음을 나타냄), 그리고 위치가 확실한 카드에 대한 정보를 통합합니다. 그런 다음 몇 가지 기본 규칙을 반복적으로 적용하여 해당 정보를 본질적으로 압축합니다.

나는 무언가를 간과했지만, 더 결정적인 정보를 얻을 수 있다고 생각하지 않습니다 (거짓 혐의로 귀찮게 가정하는 거짓 고발을 통한 경우는 제외). 그곳에 이다 플레이어 X가 카드 Y를 가지고 추정 확률에 대한보다 정교한 플레이어에 대한 잠재적 인 ...

아마도 상당한 개선을 인정할 수있는 다른 영역은 어떤 제안을할지 결정하는 것입니다. 나는 상당히 어렴풋한 헤비 포스 접근법을 사용하여 정보 획득을 극대화하려고 시도하지만 다른 가설 적 증거로부터 얻은 지식의 상대적인 장점을 평가하는 데는 상당히 정당화되지 않은 휴리스틱이 많이 있습니다. 그러나 다른 누군가가 가치있는 상대를 게시 할 때까지 휴리스틱을 조정하려고하지 않습니다.


SimpleCluedoPlayer 또는 InferencePlayer를 실행할 수 없습니다. "SpeedClueContest / entries / peter_taylor /"디렉토리에서 ant를 실행하여 JAR을 성공적으로 생성했습니다. 이 JAR에 대한 상대 및 절대 경로를 시도하여 "식별자"및 "portNumber"를 순서대로 전달했지만 TestServer는 각각의 "식별자"메시지를 기다리면서 정지합니다. "/tmp/speed-cluedo-player"+identifier+".log"를 찾아 찾을 수 없습니다. 어떻게 든 프로세스를 망쳐 놓았습니까?
sadakatsu

@ gamecoder, 아마 하드 코딩해서는 안됩니다 /tmp. 간단한 패치 여야합니다. 곧 살펴 보겠습니다.
피터 테일러

1
당신의 수정은 작동합니다; 나는 그것을 repo에 병합했습니다. 지금은주의 깊게 확인> ___ <작업을 시작했다 그것과 LogicalAI I 사이에 충분한 차이가 만들어 InferencePlayer을 통해 읽을 수
sadakatsu

여기에 상대가 온다 :-)
Ray

@ 레이, 훌륭합니다. AI를 해부하고 어떤 점에서 AI와 어떻게 다른지 살펴 보겠습니다. 한 눈에 비슷한 분석을 사용하는 것 같습니다.
피터 테일러

2

CluePaddle (ClueStick / ClueBat / ClueByFour)-C #

AI를 구현하는 것이 쉬운 C # 클라이언트 인 ClueBot과 CluePaddle이라는 가장 심각한 시도를 포함한 다양한 AI를 작성했습니다. 코드는 https://github.com/jwg4/SpeedClueContest/tree/clue_paddle에 있습니다. 에 있으며 풀 요청 를 업스트림으로 병합하기 시작했습니다.

ClueStick은 기본적으로 발생하는 대부분의 내용을 추측하고 무시하는 개념 증명입니다. ClueBat은 ClueStick의 결함을 악용하여 허위 고발을 시도하는 것을 제외하고는 또 다른 어리석은 AI입니다. ClueByFour는 합리적인 제안을하고 다른 사람들이 보여준 카드를 기억한다는 점에서 합리적인 AI입니다.

CluePaddle이 가장 지능적입니다. 그것은 어떤 디스 패킹이 제공되었을뿐만 아니라 어떤 플레이어가 주어진 제안에 대한 디프 레이션을 제공하지 않았는 지에 기초하여 누가 무엇을 가지고 있는지 파악하려고 시도합니다. 각 플레이어가 가지고있는 카드의 수는 고려하지 않지만이 문제는 해결됩니다. 여기에는 꽤 긴 클래스가 포함되어 있으므로 전체 코드를 여기에 게시하지는 않지만 다음 방법은 맛을냅니다.

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는 연결 재설정 오류를보고합니다. 내가 잘못한 일인지 판단하는 데 도움을 줄 수 있습니까?
sadakatsu

수정 : ClueStick시작하려고 할 때 멈추는 유일한 AI입니다. 다른 두 선수는 예선 토너먼트에서 경쟁하며 결국 동일한 위반으로 실격 처리됩니다. ClueByFour카드를 보유하고 있지 않을 때 비난으로 제시되는 반증되지 않은 제안을 반복하지 않으면 실격 처리됩니다. ClueBat카드가 있거나 손에 든 카드가 있다고 비난을 받으면 실격 처리됩니다. 확인하시기 바랍니다 개정 된 AI 제한을 준수하기 위해.
sadakatsu

@gamecoder NUnit이 설치되어 있습니까? 설치할 수 없다면 조건부로 유닛 테스트 코드를 제거 할 수 있습니다. CluePaddle은 실제 출품작입니다. 다른 두 선수의 위반에 대해서는 걱정하지 않아도됩니다.
jwg

에 대한 업데이트도 있습니다 CluePaddle. 나중에 이것에 대한 풀 요청을 할 것입니다.
jwg

NUnit을 설치했습니다. 다른 프로젝트 참조를 사용하여 MSVS에서 네임 스페이스를 탐색 할 수있었습니다. 풀 요청을 기다리겠습니다.
sadakatsu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.