Король пагорба: Швидкий ключ AI


24

Швидкість підказки

Cluedo / Clue - класична настільна гра з переконливим компонентом геймплею відрахування. Speed ​​Clue - це варіант 3-6 гравців, який підкреслює цей компонент, використовуючи лише карти. Результат полягає в тому, що єдина відмінність між стандартним Cluedo та Speed ​​Clue полягає в тому, що кожен гравець, який все ще знаходиться в грі, може зробити будь-яку пропозицію, яка йому подобається, на свою чергу, замість того, щоб чекати, щоб дістатись до певної кімнати на милість рулонів з кістки та пропозицій інших гравців. Якщо ви ніколи не грали в Cluedo або хочете бути впевнені в явних відмінностях між двома версіями, ви можете знайти тут повне правило Speed ​​Clue .


Мета

Написати та подати програму AI для відтворення Speed ​​Clue до 15 травня 2014 р. 00:00 GMT. Після закінчення цього часу я проведу турнір, використовуючи всі легальні записи. Учасник, чий AI виграє більшість ігор на турнірі, виграє виклик.


Технічні характеристики AI

Ви можете писати AI майже будь-якою мовою, яку ви вибрали, використовуючи будь-які методи, якими ви користуєтесь, якщо він строго використовує протокол програми через з'єднання TCP / IP для гри в ігри з сервером. Детальне пояснення всіх обмежень можна знайти тут .


Як грати

Почніть з розсилки конкурсного сховища GitHub . Додайте каталог під entriesкаталог, названий за допомогою свого імені користувача StackExchange, і розробіть свій код у цій папці. Коли ви готові надіслати свою заявку, зробіть запит на виклик із змінами, а потім дотримуйтесь цих інструкцій, щоб оголосити про свій запис на цьому веб-сайті.

Я надав у coreкаталозі деякий код та JAR, щоб розпочати роботу; дивіться на моєму сайті приблизний посібник з матеріалів. Крім того, інші гравці подають допоміжний код на додаток до своїх записів, щоб допомогти вам встати та працювати. Виділіть трохи часу, щоб вивчити записи, і не забудьте перевірити свої записи на користь інших даних перед поданням!


Результати

Place | User         | AI                 | Result
------+--------------+--------------------+-------------------------------------------------------
    1 | gamecoder    | SpockAI            | 55.75%
    2 | Peter Taylor | InferencePlayer    | 33.06%
    3 | jwg          | CluePaddle         | 20.19%
    4 | Peter Taylor | SimpleCluedoPlayer |  8.34%
    5 | gamecoder    | RandomPlayer       |  1.71%
 ---- | ray          | 01                 | Player "ray-01" [3] sent an invalid accuse message: ""

Наведені вище результати показують відсоток виграшу у кожного кваліфікованого гравця з 25 200 дійсних матчів, в яких він брав участь. Всього було проведено 30 000 матчів, які підраховували результати, і 6 100 або близько тих, які були дисконтовані, коли 01були дискваліфіковані.

Почесна згадка повинна пройти до 01ШІ Рей . Моє первісне тестування показало, що воно було найсильнішим, і я очікував, що він виграє конкуренцію. Однак, здається, є дуже переривчаста помилка, яка, наскільки я здогадуюсь, призводить її до усунення всіх можливих рішень. Турнір закінчив усі матчі з трьома гравцями і розпочав чотири матчі (12 000 ігор у!), Коли 01виявлено помилку. Якщо я просто розглядаю таблицю матчів для трьох гравців, результати виглядають приблизно так:

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., Чому два порти?
Гастуркун

1
@PeterTaylor, я зроблю доступною копію сервера, як тільки я її напишу. Як ви думаєте, чому я даю місяць? ;)
Садакацу

@Hasturkun, Архітектура, яку я запланував для сервера, полягає в тому, що він розпочне ваші подання за допомогою командного рядка. Він обере, який порт використовуватиме кожна програма для надсилання повідомлень, щоб він міг легко ідентифікувати, яка програма є (зауважте, що протокол не містить ідентифікаторів). Крім того, кожна програма повинна знати, в який порт надсилати повідомлення, щоб сервер міг насправді отримувати повідомлення. Це два порти, які кожне подання повинно отримати як аргументи командного рядка.
садакацу

1
Єдина мережна програма, яку я написав, використовує UDP. Я вирішив використовувати TCP / IP для (1), щоб зрозуміти будь-які відмінності між двома та (2), щоб використовувати технологію, яка найкраще підтримує оновлення програвача плетіння блокування, що мені потрібно для цього.
садакацу

Відповіді:


5

AI01 - Python 3

Я не можу знайти ще кращого імені для цього :-P.

Ідентифікатор : ray-ai01

Технологія : Python 3

Вибрано : так

Аргументи :ai01.py identifier port

Опис : Робота за умовиводом. Коли кількість карток, про яку власник не знає, менша за поріг, цей ШІ починає усунути всі неможливі рішення шляхом рекурсивного глобального висновку. В іншому випадку він використовує місцеві умовиводи.

#!/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, який обробляє всі введення / виведення та дозволяє логіці працювати з простим набраним інтерфейсом. Вся справа в 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 7com.sadakatsu.clue.jar

Аргументи: {identifier} portNumber [logOutput: true|false]

Опис:

SpockAI- програвач Speed ​​Clue, побудований на класі, Knowledgeякий я називав . KnowledgeКлас представляє всі можливі стани , що гра могла б дати то , що сталося до сих пір. Він представляє рішення гри та можливі руки гравців як набори, і використовує ітеративні відрахування, щоб зменшити ці набори наскільки це можливо кожного разу, коли щось дізнається. SpockAIвикористовує цей клас, щоб визначити, які пропозиції гарантують найбільш корисні результати в гіршому випадку, і випадковим чином вибирає одну з цих пропозицій по черзі. Коли йому потрібно спростувати пропозицію, вона намагається або показати карту, на якій вона вже показала запропонований AI, або показати картку з тієї категорії, для якої вона найменше зменшила можливості. Він звинувачує лише тоді, коли знає рішення.

Евристичний, який я використовував для визначення найкращої пропозиції, полягає в наступному. Після того, як вся інформація буде засвоєна з пропозиції, можливе рішення та можливі набори гравців будуть зменшені (якщо тільки пропозиція не виявить нової інформації). Теоретично найкраща пропозиція - це та, яка найбільше зменшує кількість можливих рішень. Що стосується поєдинку, я вважаю, що пропозиція, яка найбільше зменшує кількість можливих рук для гравців, є кращою. Отже, для кожної пропозиції я намагаюся досягти всіх можливих результатів, які не призводять до суперечності знань. Незалежно від того, який результат має найменше поліпшення підрахунку рішень / рук, вважається результатом, який матиме пропозиція. Потім я порівнюю всі результати пропозицій і вибираю, який з них має найкращий результат. Таким чином я гарантую оптимальний приріст інформації в гіршому випадку.

Я розглядаю можливість додавання грубого аналізу комбінації можливих рішень та можливих рук гравця, щоб зробити SpockAIще сильнішими, але оскільки SpockAIце вже найповільніший, найвитратніший ресурс, я, мабуть, пропущу це.

Відмова від відповідальності:

Я мав намір випустити AI для цього конкурсу тижнів тому. Як відомо, я не зміг почати писати свій ІІ до п’ятниці минулого тижня, і я постійно знаходив смішні помилки у своєму коді. Через це єдиний спосіб, коли мені вдалося приступити SpockAIдо роботи до встановленого терміну, було використовувати великий пул потоків. Кінцевим результатом є те, що (в даний час) SpockAI може вражати + 90% використання процесора та використання 2 ГБ + пам’яті (хоча я звинувачую у цьому збирач сміття). Я маю намір балотуватися 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, передаючи їм "ідентифікатор" та "портNumber" у такому порядку, але TestServer висить у очікуванні повідомлення "ідентифікатор живий" для кожного з них. Я шукав і не можу знайти "/tmp/speed-cluedo-player"+identifier+".log". Я якось зіпсував процес?
sadakatsu

@gamecoder, я, мабуть, не повинен жорстко кодувати /tmp. Це повинен бути простий пластир; Я незабаром розберуся.
Пітер Тейлор

1
Виправлення працює; Я об'єднав це в репо. Тепер я ретельно читаю через InferencePlayer, щоб переконатися, що існує достатня різниця між ним та LogicalAI, над яким я почав працювати> ___ <
sadakatsu

Ось ваш суперник :-)
Рей

@Ray, чудово. Я спробую розрізати ваш AI і побачити, як він відрізняється від мого в якийсь момент: на короткий погляд, здається, використовується аналогічний аналіз.
Пітер Тейлор

2

CluePaddle (ClueStick / ClueBat / ClueByFour) - C #

Я написав ClueBot, клієнт C #, для якого просто реалізувати AI, і різні AI, включаючи найсерйознішу спробу CluePaddle. Код знаходиться за адресою https://github.com/jwg4/SpeedClueContest/tree/clue_paddle із запитом на тягу, який почав об'єднувати його у верхній потік.

ClueStick - це концепція, яка в основному просто здогадується і ігнорує більшість того, що відбувається. ClueBat - це ще один дурний штучний інтелект, за винятком того, що він намагається використати недолік у ClueStick, щоб змусити його робити неправдиві звинувачення. ClueByFour є розумним інтелектуальним інструментом, оскільки він робить розумні пропозиції та запам'ятовує картки, які їх показують інші.

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);
  }
}

Якщо четверо грають один проти одного, CluePaddle виграє набагато більшість ігор, а ClueByFour другий, а двох інших ніде.

Тільки CluePaddle є конкурентоспроможною заявою (поки що). Використання:

CluePaddle.exe identifier port

Якщо хтось хоче зробити CI AI, просто створіть проект ConsoleApplication в soltution, реалізуйте IClueAIінтерфейс у класі, а потім зробіть свій Programрезультат ProgramTemplateі скопіюйте те, для чого роблять інші проекти Main(). Єдина залежність - NUnit для тестування одиниць, і ви можете легко видалити все тестування з коду (але не потрібно, просто встановіть NUnit).


Я спробував скласти ваші ІІ і перевірити їх у ContestServer (незабаром буде розміщено). CluePaddleПроект не компілюється, стверджуючи , що NUnitне встановлено , навіть якщо інші проекти робити компіляції. Ті, хто компілюють, закінчуються затримкою під час тестування, а Java повідомляє про помилку скидання з'єднання. Не могли б ви допомогти мені визначити, чи я зробив щось не так?
садакацу

Виправлення: ClueStickце єдиний ШІ, який зупиняється, коли я намагаюся його запустити. Інші двоє змагаються в пробному турнірі і в підсумку отримують дискваліфікацію за ті самі порушення. ClueByFourотримує дискваліфікацію за те, що не повторив неприйняту пропозицію, яку вона висуває як звинувачення, коли не тримає жодної з карток. ClueBatотримують дискваліфікацію за звинувачення в тому, що в ньому є картки, які вони або показали, або в руці. Будь ласка, перегляньте переглянуті обмеження щодо ІС, щоб забезпечити дотримання.
садакацу

@gamecoder У вас встановлений NUnit? Якщо ви не можете встановити його, я можу умовно видалити код тестування пристрою. CluePaddle - це мій фактичний запис - я не надто переживаю за порушення з боку двох інших, оскільки вони насправді не грають, щоб виграти.
jwg

У мене також є деякі оновлення до CluePaddle. Пізніше я звернусь із цим запитом.
jwg

Я встановив NUnit. Мені вдалося дослідити його простори імен у MSVS за допомогою посилань на ваші інші проекти. Я з нетерпінням чекаю вашого запиту на притягнення.
садакацу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.