King of the Hill: Speed ​​Clue AI

24

Pista de velocidad

Cluedo / Clue es un clásico juego de mesa con un atractivo componente de deducción. Speed ​​Clue es una variante de 3-6 jugadores que enfatiza este componente al usar solo las cartas. El resultado es que la única diferencia entre Cluedo estándar y Speed ​​Clue es que cada jugador que todavía está en el juego puede hacer cualquier sugerencia que le plazca en su turno en lugar de esperar para llegar a una habitación específica a merced de los dados y las sugerencias de otros jugadores. Si nunca antes has jugado a Cluedo, o quieres estar seguro de las diferencias explícitas entre las dos versiones, puedes encontrar una regla completa de Speed ​​Clue aquí .


Gol

Escriba y envíe un programa de IA para jugar Speed ​​Clue antes del 15 de mayo de 2014 a las 00:00 GMT. Después de ese tiempo, organizaré un torneo con todas las entradas legales. El participante cuya IA gana más juegos en el torneo gana el desafío.


Especificaciones AI

Puedes escribir tu IA en casi cualquier idioma que elijas, usando las técnicas que uses, siempre que use estrictamente el protocolo de aplicación a través de una conexión TCP / IP para jugar con el servidor. Puede encontrar una explicación detallada de todas las restricciones aquí .


Cómo jugar

Comience por bifurcar el repositorio del concurso GitHub . Agregue un directorio debajo del entriesdirectorio nombrado usando su nombre de usuario StackExchange y desarrolle su código en esa carpeta. Cuando esté listo para enviar su entrada, haga una solicitud de extracción con sus revisiones, luego siga estas instrucciones para anunciar su entrada en este sitio.

He proporcionado algunos códigos y JAR en el coredirectorio para que pueda comenzar; visite mi sitio para obtener una guía aproximada de los materiales. Además, otros jugadores envían códigos de ayuda además de sus entradas para ayudarlo a ponerse en marcha. ¡Tómese un tiempo para explorar las entradas y no olvide probar su entrada con las entradas de otros antes de enviarla!


Resultados

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: ""

Los resultados anteriores muestran el porcentaje de victorias que tuvo cada IA ​​calificada de los 25.200 partidos válidos en los que participó. Hubo 30,000 coincidencias en total que contaron para los resultados, y 6,100 más o menos que se descontaron cuando 01fue descalificado.

Una mención de honor debe ir a la 01IA de Ray . Mi prueba inicial mostró que era la más fuerte y esperaba que ganara la competencia. Sin embargo, parece tener un error muy intermitente que, hasta donde puedo adivinar, lo lleva a eliminar todas las soluciones posibles. El torneo había terminado todos los partidos de tres jugadores y había comenzado los partidos de cuatro jugadores (¡12,000 juegos en!) Cuando 01se reveló el error. Si solo considero la clasificación de partidos de 3 jugadores, los resultados se verán así:

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%

Había planeado hacer una extracción de datos sobre los resultados, pero estoy agotado. Tuve dificultades técnicas para lograr que la competencia se desarrollara por completo (fallas de energía, reinicios del sistema) que requirieron reescribir completamente el servidor del concurso para guardar su progreso a medida que avanzaba. Comentaré y comprometeré todos los cambios en el código con todos los archivos de resultados que se generaron en caso de que alguien todavía esté interesado. Si decido hacer la minería de datos también, mis resultados también se agregarán al repositorio.


¡Gracias por jugar!

sadakatsu
fuente
44
¿Puede hacer que una copia de su servidor esté disponible para que los participantes la prueben?
Peter Taylor
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.¿Por qué dos puertos?
Hasturkun
1
@ PeterTaylor, haré una copia del servidor disponible tan pronto como lo haya escrito. ¿Por qué crees que estoy dando un mes? ;)
sadakatsu
@Hasturkun, la arquitectura que he planeado para el servidor es que iniciará sus envíos a través de la línea de comandos. Escogerá qué puerto usará cada programa para enviarle mensajes para que pueda identificar fácilmente qué programa es cuál (tenga en cuenta que el protocolo no incluye ningún identificador). Además, cada programa necesita saber a qué puerto enviar mensajes para que el servidor pueda recibir mensajes. Estos son los dos puertos que cada envío debe recibir como argumentos de línea de comando.
sadakatsu el
1
El único programa de red que he escrito utiliza UDP. Decidí usar TCP / IP para (1) entender cualquier diferencia entre los dos y (2) para usar la tecnología que mejor respalde las actualizaciones del reproductor de bloqueo de pasos que necesito para que esto funcione.
sadakatsu

Respuestas:

5

AI01 - Python 3

No puedo encontrar un mejor nombre para él todavía :-P.

Identificador : ray-ai01

Tecnología : Python 3

Seleccionado : si

Argumentos :ai01.py identifier port

Descripción : Trabajo por inferencia. Cuando el número de tarjetas que el propietario desconoce es inferior a un umbral, esta IA comienza a eliminar todas las soluciones imposibles por inferencia global recursiva. De lo contrario, utiliza inferencia local.

#!/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)

El código AI se puede encontrar aquí .

Rayo
fuente
¿Podría hacer una solicitud de extracción con su IA? Me gustaría incluirlo en el repositorio del concurso.
Sadakatsu
@gamecoder Hice un AI01 más fuerte y envié una solicitud de extracción.
Ray
1
Tal como están las cosas actualmente, su 01 es el más fuerte. En las pruebas que corro, siempre gana ~ 67% de los partidos en los que compite. Espero ver algunas entradas sólidas antes del final del concurso que puedan desafiarlo.
sadakatsu
Mira mi SpockAI. Se desempeña bastante bien en contra 01. No sé si ganará la competencia, pero me alegra ver reducido su recuento de victorias; )
sadakatsu
@gamecoder En realidad, actualicé mi IA hace varios días de acuerdo con las nuevas reglas. Me alegra ver tu nueva entrada. Parece funcionar bien, pero no lo probé muchas veces debido a que es ineficiente. Tal vez pueda hacerlo más rápido para que podamos hacer la prueba más fácilmente.
Ray
4

SimpleCluedoPlayer.java

Esta clase usa AbstractCluedoPlayer, que maneja todas las E / S y permite que la lógica funcione con una interfaz simple escrita. Todo está en github .

Esto supera al jugador aleatorio con alta probabilidad (en el peor de los casos, toma 15 sugerencias, mientras que el jugador aleatorio toma un promedio de 162), pero será fácilmente derrotado. Lo ofrezco para que la pelota ruede.

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);
        }
    }
}
Peter Taylor
fuente
Muy bonito, código limpio. Dudo que alguien que mira el servidor de prueba o un jugador aleatorio se sienta así. También creo que no puedes tener una IA más simple que esta ^ _ ^
sadakatsu
4

SpockAI

Identificador: gamecoder-SpockAI

Repo Entry: haga clic aquí

Seleccionado:

Tecnología: Java 7 basado encom.sadakatsu.clue.jar

Argumentos: {identifier} portNumber [logOutput: true|false]

Descripción:

SpockAIes un jugador Speed ​​Clue construido sobre una clase llamada Knowledgeque escribí. La Knowledgeclase representa todos los estados posibles que el juego podría haber dado lo que ha sucedido hasta ahora. Representa las soluciones del juego y las posibles manos de los jugadores como conjuntos, y utiliza deducciones iterativas para reducir estos conjuntos lo más posible cada vez que se aprende algo. SpockAIusa esta clase para determinar qué sugerencias se aseguran de tener los peores resultados más útiles y selecciona aleatoriamente una de esas sugerencias en sus turnos. Cuando necesita refutar una sugerencia, intenta mostrar una carta que ya ha mostrado la IA sugerida o mostrar una carta de la categoría para la cual ha reducido las posibilidades lo más mínimo posible. Solo hace una acusación cuando conoce la solución.

La heurística que utilicé para determinar la mejor sugerencia es la siguiente. Después de que toda la información se haya aprendido de una sugerencia, la posible solución y los posibles conjuntos de manos del jugador se habrán reducido (a menos que la sugerencia no revele nueva información). Teóricamente, la mejor sugerencia es la que más reduce el número de posibles soluciones. En el caso de un empate, supongo que es mejor una sugerencia que reduzca más el número de manos posibles para los jugadores. Entonces, para cada sugerencia, intento todos los resultados posibles que no conduzcan a una contradicción en el conocimiento. Se supone que el resultado que tenga la menor mejora en el recuento de soluciones / manos es el resultado que tendrá la sugerencia. Luego comparo todos los resultados de las sugerencias y elijo cuál tiene el mejor resultado. De esta manera, garantizo una ganancia óptima de información en el peor de los casos.

Estoy considerando agregar un análisis de combinación de fuerza bruta de posibles soluciones y posibles manos de jugadores para hacer SpockAIaún más fuerte, pero como ya SpockAIes la entrada más lenta y que requiere más recursos, probablemente me saltearé eso.

Renuncia:

Tenía la intención de lanzar una IA para este concurso hace semanas. Tal como están las cosas, no pude comenzar a escribir mi IA hasta el viernes de la semana pasada, y seguí encontrando errores ridículos en mi código. Debido a esto, la única forma en que pude llegar SpockAIal trabajo antes de la fecha límite fue usando un gran grupo de subprocesos. El resultado final es que (actualmente) SpockAI puede alcanzar un uso de CPU de + 90% y un uso de memoria de 2GB + (aunque culpo al recolector de basura por esto). Tengo la intención de participar SpockAIen el concurso, pero si otros creen que es una violación de las reglas , otorgaré el título de "ganador" al segundo lugar en caso de SpockAIganar. Si te sientes así, deja un comentario al respecto en esta respuesta.

sadakatsu
fuente
3

InferencePlayer.java

Código completo en Github (nota: esto usa lo mismo AbstractCluedoPlayerque el anterior SimpleCluedoPlayer).

El verdadero núcleo de este jugador es su PlayerInformationclase (aquí ligeramente recortada):

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

Unifica la información sobre las sugerencias que el jugador no refutó (lo que indica que no tienen ninguna de esas cartas), las sugerencias que sí refutaron (lo que indica que tienen al menos una de esas cartas) y las cartas cuya ubicación es segura. Luego, aplica iterativamente algunas reglas básicas para compactar esa información en su esencia.

No creo que se pueda obtener más información determinista (excepto a través de acusaciones falsas, que supongo que es demasiado raro para molestar), aunque es posible que haya pasado por alto algo. No es posible para un jugador más sofisticado para estimar las probabilidades de que el jugador X tiene tarjeta de Y ...

La otra área que probablemente admite una mejora significativa es decidir qué sugerencia hacer. Intento maximizar la ganancia de información usando un enfoque de fuerza pesada bastante torpe, pero hay una gran cantidad de heurística mal justificada en la evaluación de los méritos relativos del conocimiento obtenido de diferentes pruebas hipotéticas. Sin embargo, no voy a intentar ajustar la heurística hasta que alguien más publique un oponente digno.

Peter Taylor
fuente
No puedo ejecutar ni tu SimpleCluedoPlayer ni tu InferencePlayer. Ejecuté hormiga en el directorio "SpeedClueContest / entries / peter_taylor /" y generé con éxito los JAR. He intentado rutas relativas y absolutas a estos JAR, pasándoles "identificador" y "número de puerto" en ese orden, pero TestServer se cuelga esperando el mensaje "identificador vivo" para cada uno de ellos. Busqué y no puedo encontrar "/tmp/speed-cluedo-player"+identifier+".log". ¿He estropeado el proceso de alguna manera?
sadakatsu
@gamecoder, probablemente no debería codificar /tmp. Debería ser un parche simple; Lo investigaré en breve.
Peter Taylor
1
Tu solución funciona; Lo he fusionado en el repositorio. Ahora puedo leer cuidadosamente InferencePlayer para asegurarme de que haya suficientes diferencias entre él y el LogicalAI en el que había comenzado a trabajar> ___ <
sadakatsu
Aquí viene tu oponente :-)
Ray
@ Ray, excelente. Intentaré diseccionar su IA y ver cómo se diferencia de la mía en algún momento: a simple vista, parece utilizar un análisis similar.
Peter Taylor
2

CluePaddle (ClueStick / ClueBat / ClueByFour) - C #

He escrito ClueBot, un cliente de C # para el que es sencillo implementar IA y varias IA, incluido el intento más serio llamado CluePaddle. El código se encuentra en https://github.com/jwg4/SpeedClueContest/tree/clue_paddle con una solicitud de extracción que comenzó a fusionarse en sentido ascendente.

ClueStick es una prueba de concepto que básicamente solo adivina e ignora la mayoría de lo que sucede. ClueBat es otra estúpida IA, excepto que intenta explotar una falla en ClueStick para obligarlo a hacer falsas acusaciones. ClueByFour es una IA razonable ya que hace sugerencias razonables y recuerda las tarjetas que otros muestran.

CluePaddle es el más inteligente. Trata de averiguar quién tiene qué basado no solo en las pruebas que se han ofrecido, sino también en qué jugadores no ofrecieron una prueba de una sugerencia dada. No tiene en cuenta cuántas cartas tiene cada jugador atm pero esto debe ser arreglado. Incluye un par de clases bastante largas, por lo que no publicaré todo el código aquí, pero el siguiente método le da un sabor.

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

Si los 4 juegan uno contra el otro, CluePaddle gana con mucho la mayoría de los juegos, con ClueByFour segundo y los otros dos en ninguna parte.

Solo CluePaddle es una entrada competitiva (hasta ahora). Uso:

CluePaddle.exe identifier port

Si alguien más quiere hacer un C # AI, simplemente cree un proyecto de ConsoleApplication en la solución, implemente la IClueAIinterfaz en una clase y luego haga su Programderivación ProgramTemplatey copie lo que hacen los otros proyectos Main(). La única dependencia es NUnit para las pruebas unitarias y puede eliminar fácilmente todas las pruebas del código (pero no lo haga, simplemente instale NUnit).

jwg
fuente
He intentado compilar sus IA y probarlas en el ContestServer (que pronto se publicará). El CluePaddleproyecto no se compila, alegando que NUnitno está instalado a pesar de que los otros proyectos sí lo hacen. Los que compilan terminan deteniéndose durante las pruebas, y Java informa un error de restablecimiento de la conexión. ¿Podría ayudarme a determinar si he hecho algo mal?
sadakatsu
Corrección: ClueStickes la única IA que se detiene cuando intento iniciarla. Los otros dos compiten en el torneo de prueba y eventualmente son descalificados por las mismas violaciones. ClueByFourqueda descalificado por no repetir una sugerencia no refutada que hace como acusación cuando no tiene ninguna de las cartas. ClueBatqueda descalificado por hacer acusaciones de que tiene cartas que se le han mostrado o que tiene en la mano. Consulte las restricciones revisadas de IA para garantizar el cumplimiento.
sadakatsu
@gamecoder ¿Tienes instalado NUnit? Si no puede instalarlo, puedo eliminar condicionalmente el código de prueba de la unidad. CluePaddle es mi entrada real: no estoy demasiado preocupado por las violaciones de los otros dos, ya que realmente no están jugando para ganar.
jwg
También tengo algunas actualizaciones para CluePaddle. Haré una solicitud de extracción para estos más tarde.
jwg
Instalé NUnit. Pude explorar sus espacios de nombres en MSVS utilizando las referencias de sus otros proyectos. Espero su solicitud de extracción.
sadakatsu