Un juego de dados, pero evita el número 6 [cerrado]

58

Torneo terminado!

¡El torneo ya terminó! La simulación final se realizó durante la noche, un total de juegos. El ganador es Christian Sievers con su bot OptFor2X . Christian Sievers también logró asegurar el segundo lugar con Rebel . ¡Felicidades! A continuación puedes ver la lista oficial de puntajes más altos para el torneo.3108

Si todavía quieres jugar, puedes usar el controlador publicado a continuación y usar el código para crear tu propio juego.

Dado

Me invitaron a jugar un juego de dados del que nunca había oído hablar. Las reglas eran simples, pero creo que sería perfecto para un desafío KotH.

Las normas

El comienzo del juego

El dado gira alrededor de la mesa, y cada vez que es tu turno, puedes lanzar el dado tantas veces como quieras. Sin embargo, debes lanzarlo al menos una vez. Lleva un registro de la suma de todos los lanzamientos de su ronda. Si elige detenerse, el puntaje de la ronda se agrega a su puntaje total.

Entonces, ¿por qué dejarías de tirar el dado? Porque si obtienes 6, tu puntaje para toda la ronda se convierte en cero, y el dado se pasa. Por lo tanto, el objetivo inicial es aumentar su puntaje lo más rápido posible.

¿Quién es el ganador?

Cuando el primer jugador alrededor de la mesa alcanza 40 puntos o más, comienza la última ronda. Una vez que la última ronda ha comenzado, todos menos la persona que inició la última ronda tienen un turno más.

Las reglas para la última ronda son las mismas que para cualquier otra ronda. Eliges seguir tirando o parar. Sin embargo, sabe que no tiene posibilidades de ganar si no obtiene una puntuación más alta que las que tenía antes en la última ronda. Pero si sigues yendo demasiado lejos, entonces podrías obtener un 6.

Sin embargo, hay una regla más a tener en cuenta. Si su puntaje total actual (su puntaje anterior + su puntaje actual para la ronda) es 40 o más, y alcanza un 6, su puntaje total se establece en 0. Eso significa que debe comenzar de nuevo. Si alcanzas un 6 cuando tu puntaje total actual es 40 o más, el juego continúa de manera normal, excepto que ahora estás en el último lugar. La última ronda no se activa cuando se restablece su puntaje total. Todavía puedes ganar la ronda, pero se vuelve más desafiante.

El ganador es el jugador con la puntuación más alta una vez que finaliza la última ronda. Si dos o más jugadores comparten el mismo puntaje, todos serán contados como vencedores.

Una regla adicional es que el juego continúa durante un máximo de 200 rondas. Esto es para evitar casos en los que varios bots básicamente continúan lanzando hasta que alcanzan 6 para mantenerse en su puntaje actual. Una vez que se pasa la 199a ronda, last_roundse establece en verdadero y se juega una ronda más. Si el juego llega a 200 rondas, el bot (o bots) con la puntuación más alta es el ganador, incluso si no tienen 40 puntos o más.

Resumen

  • En cada ronda sigues tirando el dado hasta que eliges detenerte o obtienes un 6
  • Debes lanzar el dado una vez (si tu primer lanzamiento es un 6, tu ronda termina inmediatamente)
  • Si obtiene un 6, su puntaje actual se establece en 0 (no su puntaje total)
  • Agrega su puntaje actual a su puntaje total después de cada ronda
  • Cuando un bot finaliza su turno dando como resultado una puntuación total de al menos 40, todos los demás obtienen un último turno
  • Si su puntaje total actual es y obtiene un 6, su puntaje total se establece en 0 y su ronda ha terminado40
  • La última ronda no se activa cuando ocurre lo anterior
  • La persona con el puntaje total más alto después de la última ronda es el ganador.
  • En caso de que haya múltiples ganadores, todos se contarán como ganadores.
  • El juego dura un máximo de 200 rondas.

Aclaración de las puntuaciones.

  • Puntuación total: la puntuación que has guardado de rondas anteriores
  • Puntuación actual: la puntuación de la ronda actual
  • Puntaje total actual: la suma de los dos puntajes anteriores

Como participas

Para participar en este desafío de KotH, debe escribir una clase de Python que herede de Bot. Debe implementar la función: make_throw(self, scores, last_round). Esa función se llamará una vez que sea su turno, y su primer lanzamiento no fue un 6. Para seguir lanzando, debe hacerlo yield True. Para dejar de tirar, deberías yield False. Después de cada lanzamiento, update_statese llama a la función padre . Por lo tanto, tiene acceso a sus lanzamientos para la ronda actual utilizando la variable self.current_throws. También tiene acceso a su propio índice usando self.index. Por lo tanto, para ver su propia puntuación total que usaría scores[self.index]. También puedes acceder al end_scorejuego usando self.end_score, pero puedes asumir con seguridad que será 40 para este desafío.

Se le permite crear funciones auxiliares dentro de su clase. También puede anular las funciones existentes en la Botclase principal, por ejemplo, si desea agregar más propiedades de clase. No está permitido modificar el estado del juego de ninguna manera, excepto ceder Trueo False.

Puedes buscar inspiración en esta publicación y copiar cualquiera de los dos bots que he incluido aquí. Sin embargo, me temo que no son particularmente efectivos ...

Sobre permitir otros idiomas

Tanto en el sandbox como en The Nineteenth Byte, hemos tenido discusiones sobre cómo permitir envíos en otros idiomas. Después de leer sobre tales implementaciones y escuchar argumentos de ambos lados, he decidido restringir este desafío solo a Python. Esto se debe a dos factores: el tiempo requerido para admitir múltiples idiomas y la aleatoriedad de este desafío que requiere una gran cantidad de iteraciones para alcanzar la estabilidad. Espero que sigas participando, y si quieres aprender algo de Python para este desafío, intentaré estar disponible en el chat con la mayor frecuencia posible.

Para cualquier pregunta que pueda tener, puede escribir en la sala de chat para este desafío . ¡Te veo allí!

Reglas

  • El sabotaje está permitido y alentado. Es decir, sabotaje contra otros jugadores.
  • Cualquier intento de jugar con el controlador, el tiempo de ejecución u otras presentaciones será descalificado. Todos los envíos solo deberían funcionar con las entradas y el almacenamiento que se les proporciona.
  • Cualquier bot que use más de 500 MB de memoria para tomar su decisión será descalificado (si necesita tanta memoria, debe repensar sus elecciones)
  • Un bot no debe implementar exactamente la misma estrategia que una existente, intencional o accidentalmente.
  • Se le permite actualizar su bot durante el tiempo del desafío. Sin embargo, también podría publicar otro bot si su enfoque es diferente.

Ejemplo

class GoToTenBot(Bot):
    def make_throw(self, scores, last_round):
        while sum(self.current_throws) < 10:
            yield True
        yield False

Este bot continuará hasta que tenga un puntaje de al menos 10 para la ronda, o arroje un 6. Tenga en cuenta que no necesita ninguna lógica para manejar el lanzamiento 6. También tenga en cuenta que si su primer lanzamiento es un 6, make_throwes nunca llamó, ya que su ronda ha terminado de inmediato.

Para aquellos que son nuevos en Python (y nuevos en el yieldconcepto), pero quieren probar esto, la yieldpalabra clave es similar a un retorno en algunos aspectos, pero diferente en otros. Puedes leer sobre el concepto aquí . Básicamente, una vez que usted yield, su función se detendrá, y el valor que yieldeditó será enviado de vuelta al controlador. Allí, el controlador maneja su lógica hasta que sea hora de que su bot tome otra decisión. Luego, el controlador le envía el lanzamiento de dados, y su make_throwfunción continuará ejecutándose justo donde se detuvo antes, básicamente en la línea después de la yielddeclaración anterior .

De esta manera, el controlador del juego puede actualizar el estado sin requerir una llamada a la función bot por separado para cada lanzamiento de dados.

Especificación

Puede usar cualquier biblioteca de Python disponible en pip. Para garantizar que pueda obtener un buen promedio, tiene un límite de tiempo de 100 milisegundos por ronda. Estaría muy feliz si tu guión fuera mucho más rápido que eso, para poder correr más rondas.

Evaluación

Para encontrar el ganador, tomaré todos los bots y los ejecutaré en grupos aleatorios de 8. Si hay menos de 8 clases enviadas, los ejecutaré en grupos aleatorios de 4 para evitar tener siempre todos los bots en cada ronda. Ejecutaré simulaciones durante aproximadamente 8 horas, y el ganador será el bot con el mayor porcentaje de victorias. ¡Comenzaré las simulaciones finales a principios de 2019, y te daré toda la Navidad para codificar tus bots! La fecha final preliminar es el 4 de enero, pero si es muy poco tiempo, puedo cambiarla a una fecha posterior.

Hasta entonces, intentaré hacer una simulación diaria usando 30-60 minutos de tiempo de CPU y actualizando el marcador. Este no será el puntaje oficial, pero servirá como guía para ver qué bots funcionan mejor. Sin embargo, cuando se acerca la Navidad, espero que entiendan que no estaré disponible en todo momento. Haré todo lo posible para ejecutar simulaciones y responder cualquier pregunta relacionada con el desafío.

Pruébelo usted mismo

Si desea ejecutar sus propias simulaciones, aquí está el código completo del controlador que ejecuta la simulación, incluidos dos bots de ejemplo.

Controlador

Aquí está el controlador actualizado para este desafío. Admite salidas ANSI, subprocesos múltiples y recopila estadísticas adicionales gracias a AKroell ! Cuando realice cambios en el controlador, actualizaré la publicación una vez que se complete la documentación.

Gracias a BMO , el controlador ahora puede descargar todos los bots de esta publicación usando la -dbandera. Otra funcionalidad no ha cambiado en esta versión. ¡Esto debería garantizar que todos sus últimos cambios se simulen lo antes posible!

#!/usr/bin/env python3
import re
import json
import math
import random
import requests
import sys
import time
from numpy import cumsum

from collections import defaultdict
from html import unescape
from lxml import html
from multiprocessing import Pool
from os import path, rename, remove
from sys import stderr
from time import strftime

# If you want to see what each bot decides, set this to true
# Should only be used with one thread and one game
DEBUG = False
# If your terminal supports ANSI, try setting this to true
ANSI = False
# File to keep base class and own bots
OWN_FILE = 'forty_game_bots.py'
# File where to store the downloaded bots
AUTO_FILE = 'auto_bots.py'
# If you want to use up all your quota & re-download all bots
DOWNLOAD = False
# If you want to ignore a specific user's bots (eg. your own bots): add to list
IGNORE = []
# The API-request to get all the bots
URL = "https://api.stackexchange.com/2.2/questions/177765/answers?page=%s&pagesize=100&order=desc&sort=creation&site=codegolf&filter=!bLf7Wx_BfZlJ7X"


def print_str(x, y, string):
    print("\033["+str(y)+";"+str(x)+"H"+string, end = "", flush = True)

class bcolors:
    WHITE = '\033[0m'
    GREEN = '\033[92m'
    BLUE = '\033[94m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    ENDC = '\033[0m'

# Class for handling the game logic and relaying information to the bots
class Controller:

    def __init__(self, bots_per_game, games, bots, thread_id):
        """Initiates all fields relevant to the simulation

        Keyword arguments:
        bots_per_game -- the number of bots that should be included in a game
        games -- the number of games that should be simulated
        bots -- a list of all available bot classes
        """
        self.bots_per_game = bots_per_game
        self.games = games
        self.bots = bots
        self.number_of_bots = len(self.bots)
        self.wins = defaultdict(int)
        self.played_games = defaultdict(int)
        self.bot_timings = defaultdict(float)
        # self.wins = {bot.__name__: 0 for bot in self.bots}
        # self.played_games = {bot.__name__: 0 for bot in self.bots}
        self.end_score = 40
        self.thread_id = thread_id
        self.max_rounds = 200
        self.timed_out_games = 0
        self.tied_games = 0
        self.total_rounds = 0
        self.highest_round = 0
        #max, avg, avg_win, throws, success, rounds
        self.highscore = defaultdict(lambda:[0, 0, 0, 0, 0, 0])
        self.winning_scores = defaultdict(int)
        # self.highscore = {bot.__name__: [0, 0, 0] for bot in self.bots}

    # Returns a fair dice throw
    def throw_die(self):
        return random.randint(1,6)
    # Print the current game number without newline
    def print_progress(self, progress):
        length = 50
        filled = int(progress*length)
        fill = "="*filled
        space = " "*(length-filled)
        perc = int(100*progress)
        if ANSI:
            col = [
                bcolors.RED, 
                bcolors.YELLOW, 
                bcolors.WHITE, 
                bcolors.BLUE, 
                bcolors.GREEN
            ][int(progress*4)]

            end = bcolors.ENDC
            print_str(5, 8 + self.thread_id, 
                "\t%s[%s%s] %3d%%%s" % (col, fill, space, perc, end)
            )
        else:
            print(
                "\r\t[%s%s] %3d%%" % (fill, space, perc),
                flush = True, 
                end = ""
            )

    # Handles selecting bots for each game, and counting how many times
    # each bot has participated in a game
    def simulate_games(self):
        for game in range(self.games):
            if self.games > 100:
                if game % (self.games // 100) == 0 and not DEBUG:
                    if self.thread_id == 0 or ANSI:
                        progress = (game+1) / self.games
                        self.print_progress(progress)
            game_bot_indices = random.sample(
                range(self.number_of_bots), 
                self.bots_per_game
            )

            game_bots = [None for _ in range(self.bots_per_game)]
            for i, bot_index in enumerate(game_bot_indices):
                self.played_games[self.bots[bot_index].__name__] += 1
                game_bots[i] = self.bots[bot_index](i, self.end_score)

            self.play(game_bots)
        if not DEBUG and (ANSI or self.thread_id == 0):
            self.print_progress(1)

        self.collect_results()

    def play(self, game_bots):
        """Simulates a single game between the bots present in game_bots

        Keyword arguments:
        game_bots -- A list of instantiated bot objects for the game
        """
        last_round = False
        last_round_initiator = -1
        round_number = 0
        game_scores = [0 for _ in range(self.bots_per_game)]


        # continue until one bot has reached end_score points
        while not last_round:
            for index, bot in enumerate(game_bots):
                t0 = time.clock()
                self.single_bot(index, bot, game_scores, last_round)
                t1 = time.clock()
                self.bot_timings[bot.__class__.__name__] += t1-t0

                if game_scores[index] >= self.end_score and not last_round:
                    last_round = True
                    last_round_initiator = index
            round_number += 1

            # maximum of 200 rounds per game
            if round_number > self.max_rounds - 1:
                last_round = True
                self.timed_out_games += 1
                # this ensures that everyone gets their last turn
                last_round_initiator = self.bots_per_game

        # make sure that all bots get their last round
        for index, bot in enumerate(game_bots[:last_round_initiator]):
            t0 = time.clock()
            self.single_bot(index, bot, game_scores, last_round)
            t1 = time.clock()
            self.bot_timings[bot.__class__.__name__] += t1-t0

        # calculate which bots have the highest score
        max_score = max(game_scores)
        nr_of_winners = 0
        for i in range(self.bots_per_game):
            bot_name = game_bots[i].__class__.__name__
            # average score per bot
            self.highscore[bot_name][1] += game_scores[i]
            if self.highscore[bot_name][0] < game_scores[i]:
                # maximum score per bot
                self.highscore[bot_name][0] = game_scores[i]
            if game_scores[i] == max_score:
                # average winning score per bot
                self.highscore[bot_name][2] += game_scores[i]
                nr_of_winners += 1
                self.wins[bot_name] += 1
        if nr_of_winners > 1:
            self.tied_games += 1
        self.total_rounds += round_number
        self.highest_round = max(self.highest_round, round_number)
        self.winning_scores[max_score] += 1

    def single_bot(self, index, bot, game_scores, last_round):
        """Simulates a single round for one bot

        Keyword arguments:
        index -- The player index of the bot (e.g. 0 if the bot goes first)
        bot -- The bot object about to be simulated
        game_scores -- A list of ints containing the scores of all players
        last_round -- Boolean describing whether it is currently the last round
        """

        current_throws = [self.throw_die()]
        if current_throws[-1] != 6:

            bot.update_state(current_throws[:])
            for throw in bot.make_throw(game_scores[:], last_round):
                # send the last die cast to the bot
                if not throw:
                    break
                current_throws.append(self.throw_die())
                if current_throws[-1] == 6:
                    break
                bot.update_state(current_throws[:])

        if current_throws[-1] == 6:
            # reset total score if running total is above end_score
            if game_scores[index] + sum(current_throws) - 6 >= self.end_score:
                game_scores[index] = 0
        else:
            # add to total score if no 6 is cast
            game_scores[index] += sum(current_throws)

        if DEBUG:
            desc = "%d: Bot %24s plays %40s with " + \
            "scores %30s and last round == %5s"
            print(desc % (index, bot.__class__.__name__, 
                current_throws, game_scores, last_round))

        bot_name = bot.__class__.__name__
        # average throws per round
        self.highscore[bot_name][3] += len(current_throws)
        # average success rate per round
        self.highscore[bot_name][4] += int(current_throws[-1] != 6)
        # total number of rounds
        self.highscore[bot_name][5] += 1


    # Collects all stats for the thread, so they can be summed up later
    def collect_results(self):
        self.bot_stats = {
            bot.__name__: [
                self.wins[bot.__name__],
                self.played_games[bot.__name__],
                self.highscore[bot.__name__]
            ]
        for bot in self.bots}


# 
def print_results(total_bot_stats, total_game_stats, elapsed_time):
    """Print the high score after the simulation

    Keyword arguments:
    total_bot_stats -- A list containing the winning stats for each thread
    total_game_stats -- A list containing controller stats for each thread
    elapsed_time -- The number of seconds that it took to run the simulation
    """

    # Find the name of each bot, the number of wins, the number
    # of played games, and the win percentage
    wins = defaultdict(int)
    played_games = defaultdict(int)
    highscores = defaultdict(lambda: [0, 0, 0, 0, 0, 0])
    bots = set()
    timed_out_games = sum(s[0] for s in total_game_stats)
    tied_games = sum(s[1] for s in total_game_stats)
    total_games = sum(s[2] for s in total_game_stats)
    total_rounds = sum(s[4] for s in total_game_stats)
    highest_round = max(s[5] for s in total_game_stats)
    average_rounds = total_rounds / total_games
    winning_scores = defaultdict(int)
    bot_timings = defaultdict(float)

    for stats in total_game_stats:
        for score, count in stats[6].items():
            winning_scores[score] += count
    percentiles = calculate_percentiles(winning_scores, total_games)


    for thread in total_bot_stats:
        for bot, stats in thread.items():
            wins[bot] += stats[0]
            played_games[bot] += stats[1]

            highscores[bot][0] = max(highscores[bot][0], stats[2][0])       
            for i in range(1, 6):
                highscores[bot][i] += stats[2][i]
            bots.add(bot)

    for bot in bots:
        bot_timings[bot] += sum(s[3][bot] for s in total_game_stats)

    bot_stats = [[bot, wins[bot], played_games[bot], 0] for bot in bots]

    for i, bot in enumerate(bot_stats):
        bot[3] = 100 * bot[1] / bot[2] if bot[2] > 0 else 0
        bot_stats[i] = tuple(bot)

    # Sort the bots by their winning percentage
    sorted_scores = sorted(bot_stats, key=lambda x: x[3], reverse=True)
    # Find the longest class name for any bot
    max_len = max([len(b[0]) for b in bot_stats])

    # Print the highscore list
    if ANSI:
        print_str(0, 9 + threads, "")
    else:
        print("\n")


    sim_msg = "\tSimulation or %d games between %d bots " + \
        "completed in %.1f seconds"
    print(sim_msg % (total_games, len(bots), elapsed_time))
    print("\tEach game lasted for an average of %.2f rounds" % average_rounds)
    print("\t%d games were tied between two or more bots" % tied_games)
    print("\t%d games ran until the round limit, highest round was %d\n"
        % (timed_out_games, highest_round))

    print_bot_stats(sorted_scores, max_len, highscores)
    print_score_percentiles(percentiles)
    print_time_stats(bot_timings, max_len)

def calculate_percentiles(winning_scores, total_games):
    percentile_bins = 10000
    percentiles = [0 for _ in range(percentile_bins)]
    sorted_keys = list(sorted(winning_scores.keys()))
    sorted_values = [winning_scores[key] for key in sorted_keys]
    cumsum_values = list(cumsum(sorted_values))
    i = 0

    for perc in range(percentile_bins):
        while cumsum_values[i] < total_games * (perc+1) / percentile_bins:
            i += 1
        percentiles[perc] = sorted_keys[i] 
    return percentiles

def print_score_percentiles(percentiles):
    n = len(percentiles)
    show = [.5, .75, .9, .95, .99, .999, .9999]
    print("\t+----------+-----+")
    print("\t|Percentile|Score|")
    print("\t+----------+-----+")
    for p in show:
        print("\t|%10.2f|%5d|" % (100*p, percentiles[int(p*n)]))
    print("\t+----------+-----+")
    print()


def print_bot_stats(sorted_scores, max_len, highscores):
    """Print the stats for the bots

    Keyword arguments:
    sorted_scores -- A list containing the bots in sorted order
    max_len -- The maximum name length for all bots
    highscores -- A dict with additional stats for each bot
    """
    delimiter_format = "\t+%s%s+%s+%s+%s+%s+%s+%s+%s+%s+"
    delimiter_args = ("-"*(max_len), "", "-"*4, "-"*8, 
        "-"*8, "-"*6, "-"*6, "-"*7, "-"*6, "-"*8)
    delimiter_str = delimiter_format % delimiter_args
    print(delimiter_str)
    print("\t|%s%s|%4s|%8s|%8s|%6s|%6s|%7s|%6s|%8s|" 
        % ("Bot", " "*(max_len-3), "Win%", "Wins", 
            "Played", "Max", "Avg", "Avg win", "Throws", "Success%"))
    print(delimiter_str)

    for bot, wins, played, score in sorted_scores:
        highscore = highscores[bot]
        bot_max_score = highscore[0]
        bot_avg_score = highscore[1] / played
        bot_avg_win_score = highscore[2] / max(1, wins)
        bot_avg_throws = highscore[3] / highscore[5]
        bot_success_rate = 100 * highscore[4] / highscore[5]

        space_fill = " "*(max_len-len(bot))
        format_str = "\t|%s%s|%4.1f|%8d|%8d|%6d|%6.2f|%7.2f|%6.2f|%8.2f|"
        format_arguments = (bot, space_fill, score, wins, 
            played, bot_max_score, bot_avg_score,
            bot_avg_win_score, bot_avg_throws, bot_success_rate)
        print(format_str % format_arguments)

    print(delimiter_str)
    print()

def print_time_stats(bot_timings, max_len):
    """Print the execution time for all bots

    Keyword arguments:
    bot_timings -- A dict containing information about timings for each bot
    max_len -- The maximum name length for all bots
    """
    total_time = sum(bot_timings.values())
    sorted_times = sorted(bot_timings.items(), 
        key=lambda x: x[1], reverse = True)

    delimiter_format = "\t+%s+%s+%s+"
    delimiter_args = ("-"*(max_len), "-"*7, "-"*5)
    delimiter_str = delimiter_format % delimiter_args
    print(delimiter_str)

    print("\t|%s%s|%7s|%5s|" % ("Bot", " "*(max_len-3), "Time", "Time%"))
    print(delimiter_str)
    for bot, bot_time in sorted_times:
        space_fill = " "*(max_len-len(bot))
        perc = 100 * bot_time / total_time
        print("\t|%s%s|%7.2f|%5.1f|" % (bot, space_fill, bot_time, perc))
    print(delimiter_str)
    print() 


def run_simulation(thread_id, bots_per_game, games_per_thread, bots):
    """Used by multithreading to run the simulation in parallel

    Keyword arguments:
    thread_id -- A unique identifier for each thread, starting at 0
    bots_per_game -- How many bots should participate in each game
    games_per_thread -- The number of games to be simulated
    bots -- A list of all bot classes available
    """
    try:
        controller = Controller(bots_per_game, 
            games_per_thread, bots, thread_id)
        controller.simulate_games()
        controller_stats = (
            controller.timed_out_games,
            controller.tied_games,
            controller.games,
            controller.bot_timings,
            controller.total_rounds,
            controller.highest_round,
            controller.winning_scores
        )
        return (controller.bot_stats, controller_stats)
    except KeyboardInterrupt:
        return {}


# Prints the help for the script
def print_help():
    print("\nThis is the controller for the PPCG KotH challenge " + \
        "'A game of dice, but avoid number 6'")
    print("For any question, send a message to maxb\n")
    print("Usage: python %s [OPTIONS]" % sys.argv[0])
    print("\n  -n\t\tthe number of games to simluate")
    print("  -b\t\tthe number of bots per round")
    print("  -t\t\tthe number of threads")
    print("  -d\t--download\tdownload all bots from codegolf.SE")
    print("  -A\t--ansi\trun in ANSI mode, with prettier printing")
    print("  -D\t--debug\trun in debug mode. Sets to 1 thread, 1 game")
    print("  -h\t--help\tshow this help\n")

# Make a stack-API request for the n-th page
def req(n):
    req = requests.get(URL % n)
    req.raise_for_status()
    return req.json()

# Pull all the answers via the stack-API
def get_answers():
    n = 1
    api_ans = req(n)
    answers = api_ans['items']
    while api_ans['has_more']:
        n += 1
        if api_ans['quota_remaining']:
            api_ans = req(n)
            answers += api_ans['items']
        else:
            break

    m, r = api_ans['quota_max'], api_ans['quota_remaining']
    if 0.1 * m > r:
        print(" > [WARN]: only %s/%s API-requests remaining!" % (r,m), file=stderr)

    return answers


def download_players():
    players = {}

    for ans in get_answers():
        name = unescape(ans['owner']['display_name'])
        bots = []

        root = html.fromstring('<body>%s</body>' % ans['body'])
        for el in root.findall('.//code'):
            code = el.text
            if re.search(r'^class \w+\(\w*Bot\):.*$', code, flags=re.MULTILINE):
                bots.append(code)

        if not bots:
            print(" > [WARN] user '%s': couldn't locate any bots" % name, file=stderr)
        elif name in players:
            players[name] += bots
        else:
            players[name] = bots

    return players


# Download all bots from codegolf.stackexchange.com
def download_bots():
    print('pulling bots from the interwebs..', file=stderr)
    try:
        players = download_players()
    except Exception as ex:
        print('FAILED: (%s)' % ex, file=stderr)
        exit(1)

    if path.isfile(AUTO_FILE):
        print(' > move: %s -> %s.old' % (AUTO_FILE,AUTO_FILE), file=stderr)
        if path.exists('%s.old' % AUTO_FILE):
            remove('%s.old' % AUTO_FILE)
        rename(AUTO_FILE, '%s.old' % AUTO_FILE)

    print(' > writing players to %s' % AUTO_FILE, file=stderr)
    f = open(AUTO_FILE, 'w+', encoding='utf8')
    f.write('# -*- coding: utf-8 -*- \n')
    f.write('# Bots downloaded from https://codegolf.stackexchange.com/questions/177765 @ %s\n\n' % strftime('%F %H:%M:%S'))
    with open(OWN_FILE, 'r') as bfile:
        f.write(bfile.read()+'\n\n\n# Auto-pulled bots:\n\n')
    for usr in players:
        if usr not in IGNORE:
            for bot in players[usr]:
                f.write('# User: %s\n' % usr)
                f.write(bot+'\n\n')
    f.close()

    print('OK: pulled %s bots' % sum(len(bs) for bs in players.values()))


if __name__ == "__main__":

    games = 10000
    bots_per_game = 8
    threads = 4

    for i, arg in enumerate(sys.argv):
        if arg == "-n" and len(sys.argv) > i+1 and sys.argv[i+1].isdigit():
            games = int(sys.argv[i+1])
        if arg == "-b" and len(sys.argv) > i+1 and sys.argv[i+1].isdigit():
            bots_per_game = int(sys.argv[i+1])
        if arg == "-t" and len(sys.argv) > i+1 and sys.argv[i+1].isdigit():
            threads = int(sys.argv[i+1])
        if arg == "-d" or arg == "--download":
            DOWNLOAD = True
        if arg == "-A" or arg == "--ansi":
            ANSI = True
        if arg == "-D" or arg == "--debug":
            DEBUG = True
        if arg == "-h" or arg == "--help":
            print_help()
            quit()
    if ANSI:
        print(chr(27) + "[2J", flush =  True)
        print_str(1,3,"")
    else:
        print()

    if DOWNLOAD:
        download_bots()
        exit() # Before running other's code, you might want to inspect it..

    if path.isfile(AUTO_FILE):
        exec('from %s import *' % AUTO_FILE[:-3])
    else:
        exec('from %s import *' % OWN_FILE[:-3])

    bots = get_all_bots()

    if bots_per_game > len(bots):
        bots_per_game = len(bots)
    if bots_per_game < 2:
        print("\tAt least 2 bots per game is needed")
        bots_per_game = 2
    if games <= 0:
        print("\tAt least 1 game is needed")
        games = 1
    if threads <= 0:
        print("\tAt least 1 thread is needed")
        threads = 1
    if DEBUG:
        print("\tRunning in debug mode, with 1 thread and 1 game")
        threads = 1
        games = 1

    games_per_thread = math.ceil(games / threads)

    print("\tStarting simulation with %d bots" % len(bots))
    sim_str = "\tSimulating %d games with %d bots per game"
    print(sim_str % (games, bots_per_game))
    print("\tRunning simulation on %d threads" % threads)
    if len(sys.argv) == 1:
        print("\tFor help running the script, use the -h flag")
    print()

    with Pool(threads) as pool:
        t0 = time.time()
        results = pool.starmap(
            run_simulation, 
            [(i, bots_per_game, games_per_thread, bots) for i in range(threads)]
        )
        t1 = time.time()
        if not DEBUG:
            total_bot_stats = [r[0] for r in results]
            total_game_stats = [r[1] for r in results]
            print_results(total_bot_stats, total_game_stats, t1-t0)

Si desea acceder al controlador original para este desafío, está disponible en el historial de edición. El nuevo controlador tiene exactamente la misma lógica para ejecutar el juego, la única diferencia es el rendimiento, la recopilación de estadísticas y la impresión más bonita.

Bots

En mi máquina, los bots se guardan en el archivo forty_game_bots.py. Si usa cualquier otro nombre para el archivo, debe actualizar la importdeclaración en la parte superior del controlador.

import sys, inspect
import random
import numpy as np

# Returns a list of all bot classes which inherit from the Bot class
def get_all_bots():
    return Bot.__subclasses__()

# The parent class for all bots
class Bot:

    def __init__(self, index, end_score):
        self.index = index
        self.end_score = end_score

    def update_state(self, current_throws):
        self.current_throws = current_throws

    def make_throw(self, scores, last_round):
        yield False


class ThrowTwiceBot(Bot):

    def make_throw(self, scores, last_round):
        yield True
        yield False

class GoToTenBot(Bot):

    def make_throw(self, scores, last_round):
        while sum(self.current_throws) < 10:
            yield True
        yield False

Ejecutando la simulación

Para ejecutar una simulación, guarde los dos fragmentos de código publicados anteriormente en dos archivos separados. Los he guardado como forty_game_controller.pyy forty_game_bots.py. Luego simplemente usa python forty_game_controller.pyo python3 forty_game_controller.pydepende de su configuración de Python. Siga las instrucciones a partir de ahí si desea configurar su simulación aún más, o intente jugar con el código si lo desea.

Estadísticas del juego

Si está haciendo un bot que apunta a un cierto puntaje sin tener en cuenta otros bots, estos son los percentiles de puntaje ganador:

+----------+-----+
|Percentile|Score|
+----------+-----+
|     50.00|   44|
|     75.00|   48|
|     90.00|   51|
|     95.00|   54|
|     99.00|   58|
|     99.90|   67|
|     99.99|  126|
+----------+-----+

Puntuaciones altas

A medida que se publiquen más respuestas, intentaré mantener esta lista actualizada. El contenido de la lista siempre será de la última simulación. Los bots ThrowTwiceBoty GoToTenBotson los bots del código anterior, y se usan como referencia. Hice una simulación con 10 ^ 8 juegos, que tardó aproximadamente 1 hora. Luego vi que el juego alcanzó la estabilidad en comparación con mis carreras con 10 ^ 7 juegos. Sin embargo, con la gente aún publicando bots, no haré más simulaciones hasta que la frecuencia de las respuestas haya disminuido.

Intento agregar todos los bots nuevos y agregar cualquier cambio que haya realizado a los bots existentes. Si parece que me he perdido su bot o cualquier cambio nuevo que tenga, escriba en el chat y me aseguraré de tener su última versión en la próxima simulación.

¡Ahora tenemos más estadísticas para cada bot gracias a AKroell ! Las tres nuevas columnas contienen el puntaje máximo en todos los juegos, el puntaje promedio por juego y el puntaje promedio al ganar para cada bot.

Como se señaló en los comentarios, hubo un problema con la lógica del juego que hizo que los bots que tenían un índice más alto dentro de un juego obtuvieran una ronda adicional en algunos casos. Esto se ha solucionado ahora, y las puntuaciones a continuación reflejan esto.

Simulation or 300000000 games between 49 bots completed in 35628.7 seconds
Each game lasted for an average of 3.73 rounds
29127662 games were tied between two or more bots
0 games ran until the round limit, highest round was 22

+-----------------------+----+--------+--------+------+------+-------+------+--------+
|Bot                    |Win%|    Wins|  Played|   Max|   Avg|Avg win|Throws|Success%|
+-----------------------+----+--------+--------+------+------+-------+------+--------+
|OptFor2X               |21.6|10583693|48967616|    99| 20.49|  44.37|  4.02|   33.09|
|Rebel                  |20.7|10151261|48977862|   104| 21.36|  44.25|  3.90|   35.05|
|Hesitate               |20.3| 9940220|48970815|   105| 21.42|  44.23|  3.89|   35.11|
|EnsureLead             |20.3| 9929074|48992362|   101| 20.43|  44.16|  4.50|   25.05|
|StepBot                |20.2| 9901186|48978938|    96| 20.42|  43.47|  4.56|   24.06|
|BinaryBot              |20.1| 9840684|48981088|   115| 21.01|  44.48|  3.85|   35.92|
|Roll6Timesv2           |20.1| 9831713|48982301|   101| 20.83|  43.53|  4.37|   27.15|
|AggressiveStalker      |19.9| 9767637|48979790|   110| 20.46|  44.86|  3.90|   35.04|
|FooBot                 |19.9| 9740900|48980477|   100| 22.03|  43.79|  3.91|   34.79|
|QuotaBot               |19.9| 9726944|48980023|   101| 19.96|  44.95|  4.50|   25.03|
|BePrepared             |19.8| 9715461|48978569|   112| 18.68|  47.58|  4.30|   28.31|
|AdaptiveRoller         |19.7| 9659023|48982819|   107| 20.70|  43.27|  4.51|   24.81|
|GoTo20Bot              |19.6| 9597515|48973425|   108| 21.15|  43.24|  4.44|   25.98|
|Gladiolen              |19.5| 9550368|48970506|   107| 20.16|  45.31|  3.91|   34.81|
|LastRound              |19.4| 9509645|48988860|   100| 20.45|  43.50|  4.20|   29.98|
|BrainBot               |19.4| 9500957|48985984|   105| 19.26|  45.56|  4.46|   25.71|
|GoTo20orBestBot        |19.4| 9487725|48975944|   104| 20.98|  44.09|  4.46|   25.73|
|Stalker                |19.4| 9485631|48969437|   103| 20.20|  45.34|  3.80|   36.62|
|ClunkyChicken          |19.1| 9354294|48972986|   112| 21.14|  45.44|  3.57|   40.48|
|FortyTeen              |18.8| 9185135|48980498|   107| 20.90|  46.77|  3.88|   35.32|
|Crush                  |18.6| 9115418|48985778|    96| 14.82|  43.08|  5.15|   14.15|
|Chaser                 |18.6| 9109636|48986188|   107| 19.52|  45.62|  4.06|   32.39|
|MatchLeaderBot         |16.6| 8122985|48979024|   104| 18.61|  45.00|  3.20|   46.70|
|Ro                     |16.5| 8063156|48972140|   108| 13.74|  48.24|  5.07|   15.44|
|TakeFive               |16.1| 7906552|48994992|   100| 19.38|  44.68|  3.36|   43.96|
|RollForLuckBot         |16.1| 7901601|48983545|   109| 17.30|  50.54|  4.72|   21.30|
|Alpha                  |15.5| 7584770|48985795|   104| 17.45|  46.64|  4.04|   32.67|
|GoHomeBot              |15.1| 7418649|48974928|    44| 13.23|  41.41|  5.49|    8.52|
|LeadBy5Bot             |15.0| 7354458|48987017|   110| 17.15|  46.95|  4.13|   31.16|
|NotTooFarBehindBot     |15.0| 7338828|48965720|   115| 17.75|  45.03|  2.99|   50.23|
|GoToSeventeenRollTenBot|14.1| 6900832|48976440|   104| 10.26|  49.25|  5.68|    5.42|
|LizduadacBot           |14.0| 6833125|48978161|    96|  9.67|  51.35|  5.72|    4.68|
|TleilaxuBot            |13.5| 6603853|48985292|   137| 15.25|  45.05|  4.27|   28.80|
|BringMyOwn_dice        |12.0| 5870328|48974969|    44| 21.27|  41.47|  4.24|   29.30|
|SafetyNet              |11.4| 5600688|48987015|    98| 15.81|  45.03|  2.41|   59.84|
|WhereFourArtThouChicken|10.5| 5157324|48976428|    64| 22.38|  47.39|  3.59|   40.19|
|ExpectationsBot        | 9.0| 4416154|48976485|    44| 24.40|  41.55|  3.58|   40.41|
|OneStepAheadBot        | 8.4| 4132031|48975605|    50| 18.24|  46.02|  3.20|   46.59|
|GoBigEarly             | 6.6| 3218181|48991348|    49| 20.77|  42.95|  3.90|   35.05|
|OneInFiveBot           | 5.8| 2826326|48974364|   155| 17.26|  49.72|  3.00|   50.00|
|ThrowThriceBot         | 4.1| 1994569|48984367|    54| 21.70|  44.55|  2.53|   57.88|
|FutureBot              | 4.0| 1978660|48985814|    50| 17.93|  45.17|  2.36|   60.70|
|GamblersFallacy        | 1.3|  621945|48986528|    44| 22.52|  41.46|  2.82|   53.07|
|FlipCoinRollDice       | 0.7|  345385|48972339|    87| 15.29|  44.55|  1.61|   73.17|
|BlessRNG               | 0.2|   73506|48974185|    49| 14.54|  42.72|  1.42|   76.39|
|StopBot                | 0.0|    1353|48984828|    44| 10.92|  41.57|  1.00|   83.33|
|CooperativeSwarmBot    | 0.0|     991|48970284|    44| 10.13|  41.51|  1.36|   77.30|
|PointsAreForNerdsBot   | 0.0|       0|48986508|     0|  0.00|   0.00|  6.00|    0.00|
|SlowStart              | 0.0|       0|48973613|    35|  5.22|   0.00|  3.16|   47.39|
+-----------------------+----+--------+--------+------+------+-------+------+--------+

Los siguientes bots (excepto Rebel) están hechos para romper las reglas, y los creadores han acordado no participar en el torneo oficial. Sin embargo, sigo pensando que sus ideas son creativas y merecen una mención honorífica. Rebel también está en esta lista porque usa una estrategia inteligente para evitar el sabotaje, y en realidad funciona mejor con el bot de sabotaje en juego.

Los bots NeoBoty KwisatzHaderachsiguen las reglas, pero usan un vacío al predecir el generador aleatorio. Como estos robots requieren muchos recursos para simular, he agregado sus estadísticas de una simulación con menos juegos. El bot HarkonnenBotlogra la victoria al deshabilitar todos los demás bots, lo cual es estrictamente contrario a las reglas.

    Simulation or 300000 games between 52 bots completed in 66.2 seconds
    Each game lasted for an average of 4.82 rounds
    20709 games were tied between two or more bots
    0 games ran until the round limit, highest round was 31

    +-----------------------+----+--------+--------+------+------+-------+------+--------+
    |Bot                    |Win%|    Wins|  Played|   Max|   Avg|Avg win|Throws|Success%|
    +-----------------------+----+--------+--------+------+------+-------+------+--------+
    |KwisatzHaderach        |80.4|   36986|   46015|   214| 58.19|  64.89| 11.90|   42.09|
    |HarkonnenBot           |76.0|   35152|   46264|    44| 34.04|  41.34|  1.00|   83.20|
    |NeoBot                 |39.0|   17980|   46143|   214| 37.82|  59.55|  5.44|   50.21|
    |Rebel                  |26.8|   12410|   46306|    92| 20.82|  43.39|  3.80|   35.84|
    +-----------------------+----+--------+--------+------+------+-------+------+--------+

    +----------+-----+
    |Percentile|Score|
    +----------+-----+
    |     50.00|   45|
    |     75.00|   50|
    |     90.00|   59|
    |     95.00|   70|
    |     99.00|   97|
    |     99.90|  138|
    |     99.99|  214|
    +----------+-----+
maxb
fuente
2
Entonces, tal vez las reglas serían un poco más claras si decían "cuando un jugador termina su turno con una puntuación de al menos 40, todos los demás obtienen un último turno". Esto evita el conflicto aparente al señalar que no está llegando a 40 lo que realmente desencadena la última ronda, se detiene con al menos 40.
aschepler
1
@aschepler es una buena formulación, editaré la publicación cuando esté en mi computadora
maxb
2
@maxb Extendí el controlador para agregar más estadísticas relevantes para mi proceso de desarrollo: puntaje más alto alcanzado, puntaje promedio alcanzado y puntaje promedio ganador gist.github.com/AwK/91446718a46f3e001c19533298b5756c
AKroell
2
Esto suena muy similar a un juego de dados muy divertido llamado Farkled en.wikipedia.org/wiki/Farkle
Caleb Jay
55
Estoy votando para cerrar esta pregunta porque ya está cerrada de facto a nuevas respuestas ("¡El torneo ya terminó! La simulación final se realizó durante la noche, un total de 3 ∗ 108 juegos")
pppery

Respuestas:

6

OptFor2X

Este bot sigue una aproximación a la estrategia óptima para la versión de dos jugadores de este juego, utilizando solo su puntaje y el puntaje del mejor oponente. En la última ronda, la versión actualizada considera todos los puntajes.

class OptFor2X(Bot):

    _r = []
    _p = []

    def _u(self,l):
        res = []
        for x in l:
            if isinstance(x,int):
                if x>0:
                    a=b=x
                else:
                    a,b=-2,-x
            else:
                if len(x)==1:
                    a = x[0]
                    if a<0:
                        a,b=-3,-a
                    else:
                        b=a+2
                else:
                    a,b=x
            if a<0:
                res.extend((b for _ in range(-a)))
            else:
                res.extend(range(a,b+1))
        res.extend((res[-1] for _ in range(40-len(res))))
        return res


    def __init__(self,*args):
        super().__init__(*args)
        if self._r:
            return
        self._r.append(self._u([[-8, 14], -15, [-6, 17], [18, 21], [21],
                                 -23, -24, 25, [-3, 21], [22, 29]]))
        self._r.extend((None for _ in range(13)))
        self._r.extend((self._u(x) for x in
                   ([[-19, 13], [-4, 12], -13, [-14], [-5, 15], [-4, 16],
                     -17, 18],
                    [[-6, 12], [-11, 13], [-4, 12], -11, -12, [-13], [-14],
                     [-5, 15], -16, 17],
                    [11, 11, [-10, 12], -13, [-24], 13, 12, [-6, 11], -12,
                     [-13], [-6, 14], -15, 16],
                    [[-8, 11], -12, 13, [-9, 23], 11, [-10], [-11], [-12],
                     [-5, 13], -14, [14]],
                    [[-4, 10], [-11], 12, [-14, 22], 10, 9, -10, [-4, 11],
                     [-5, 12], -13, -14, 15],
                    [[-4, 10], 11, [-18, 21], [-9], [-10], [-5, 11], [-12],
                     -13, 14],
                    [[-24, 20], [-5, 9], [-4, 10], [-4, 11], -12, 13],
                    [[-25, 19], [-8], [-4, 9], [-4, 10], -11, 12],
                    [[-26, 18], [-5, 8], [-5, 9], 10, [10]],
                    [[-27, 17], [-4, 7], [-5, 8], 9, [9]],
                    [[-28, 16], -6, [-5, 7], -8, -9, 10],
                    [[-29, 15], [-5, 6], [-7], -8, 9],
                    [[-29, 14], [-4, 5], [-4, 6], [7]],
                    [[-30, 13], -4, [-4, 5], 6, [6]], 
                    [[-31, 12], [-5, 4], 5, [5]],
                    [[-31, 11], [-4, 3], [3], 5, 6],
                    [[-31, 10], 11, [-2], 3, [3]],
                    [[-31, 9], 10, 2, -1, 2, [2]],
                    [[-31, 8], 9, [-4, 1], [1]],
                    [[-30, 7], [7], [-5, 1], 2],
                    [[-30, 6], [6], 1],
                    [[-31, 5], [6], 1],
                    [[-31, 4], [5, 8], 1],
                    [[-31, 3], [4, 7], 1],
                    [[-31, 2], [3, 6], 1],
                    [[-31, 1], [2, 10]] ) ))
        l=[0.0,0.0,0.0,0.0,1.0]
        for i in range(300):
            l.append(sum([a/6 for a in l[i:]]))
        m=[i/6 for i in range(1,5)]
        self._p.extend((1-sum([a*b for a,b in zip(m,l[i:])])
                                           for i in range(300)))

    def update_state(self,*args):
        super().update_state(*args)
        self.current_sum = sum(self.current_throws)

    def expect(self,mts,ops):
        p = 1.0
        for s in ops:
            p *= self._p[mts-s]
        return p

    def throw_again(self,mts,ops):
        ps = self.expect(mts,ops)
        pr = sum((self.expect(mts+d,ops) for d in range(1,6)))/6
        return pr>ps

    def make_throw(self,scores,last_round):
        myscore=scores[self.index]
        if last_round:
            target=max(scores)-myscore
            if max(scores)<40:
                opscores = scores[self.index+1:]
            else:
                opscores = []
                i = (self.index + 1) % len(scores)
                while scores[i] < 40:
                    opscores.append(scores[i])
                    i = (i+1) % len(scores)
        else:
            opscores = [s for i,s in enumerate(scores) if i!=self.index]
            bestop = max(opscores)
            target = min(self._r[myscore][bestop],40-myscore)
            # (could change the table instead of using min)
        while self.current_sum < target:
            yield True
        lr = last_round or myscore+self.current_sum >= 40
        while lr and self.throw_again(myscore+self.current_sum,opscores):
            yield True
        yield False
Christian Sievers
fuente
Revisaré la implementación tan pronto como pueda. Con las celebraciones navideñas, puede que no sea hasta el 25
maxb
¡Tu bot está a la cabeza! Además, no hay necesidad de hacerlo correr más rápido, es aproximadamente tan rápido como todos los demás bots para tomar decisiones.
maxb
No quería hacerlo más rápido. Ya hice lo que quería hacer, inicializar solo una vez, pero estaba buscando una mejor manera de hacerlo, especialmente sin definir funciones fuera de la clase. Creo que es mejor ahora.
Christian Sievers
Se ve mucho mejor ahora, ¡buen trabajo!
maxb
¡Felicitaciones por asegurar el primer y segundo lugar!
maxb
20

NeoBot

En cambio, solo trate de darse cuenta de la verdad: no hay cuchara

NeoBot se asoma a la matriz (también conocido como aleatorio) y predice si el próximo lanzamiento será un 6 o no, no puede hacer nada para recibir un 6 para comenzar, pero está más que feliz de esquivar un ender de racha.

NeoBot en realidad no modifica el controlador o el tiempo de ejecución, solo pide cortésmente a la biblioteca más información.

class NeoBot(Bot):
    def __init__(self, index, end_score):
        self.random = None
        self.last_scores = None
        self.last_state = None
        super().__init__(index,end_score)

    def make_throw(self, scores, last_round):
        while True:
            if self.random is None:
                self.random = inspect.stack()[1][0].f_globals['random']
            tscores = scores[:self.index] + scores[self.index+1:]
            if self.last_scores != tscores:
                self.last_state = None
                self.last_scores = tscores
            future = self.predictnext_randint(self.random)
            if future == 6:
                yield False
            else:
                yield True

    def genrand_int32(self,base):
        base ^= (base >> 11)
        base ^= (base << 7) & 0x9d2c5680
        base ^= (base << 15) & 0xefc60000
        return base ^ (base >> 18)

    def predictnext_randint(self,cls):
        if self.last_state is None:
            self.last_state = list(cls.getstate()[1])
        ind = self.last_state[-1]
        width = 6
        res = width + 1
        while res >= width:
            y = self.last_state[ind]
            r = self.genrand_int32(y)
            res = r >> 29
            ind += 1
            self.last_state[-1] = (self.last_state[-1] + 1) % (len(self.last_state))
        return 1 + res
Sobre todo inofensivo
fuente
1
Bienvenido a PPCG! Esta es una respuesta realmente impresionante. Cuando lo ejecuté por primera vez, me molestó el hecho de que usaba la misma cantidad de tiempo de ejecución que todos los demás robots combinados. Luego miré el porcentaje de victorias. Manera realmente inteligente de eludir las reglas. Permitiré que tu bot participe en el torneo, pero espero que otros se abstengan de usar la misma táctica, ya que viola el espíritu del juego.
maxb
2
Dado que existe una brecha tan grande entre este bot y el segundo lugar, combinado con el hecho de que su bot requiere mucha computación, ¿aceptaría que ejecute una simulación con menos iteraciones para encontrar su tasa de ganancia y luego ejecute el oficial simulación sin tu bot?
maxb
3
Bien por mí, pensé al entrar que esto era probablemente descalificable y definitivamente no del todo en el espíritu del juego. Dicho esto, fue una maravilla ponerse a trabajar y una excusa divertida para hurgar en el código fuente de Python.
Mayormente inofensivo el
2
¡Gracias! No creo que ningún otro bot se acerque a tu puntaje. Y para cualquiera que esté pensando en implementar esta estrategia, no lo haga. De ahora en adelante, esta estrategia va en contra de las reglas, y NeoBot es el único que puede usarlo para mantener el torneo justo.
maxb
1
Bueno, myBot supera a todos, pero esto es mucho mejor, aunque si publicara un bot como este, obtendría -100 y no la mejor puntuación.
Jan Ivan
15

Enjambre cooperativo

Estrategia

No creo que nadie más haya notado la importancia de esta regla:

Si el juego llega a 200 rondas, el bot (o bots) con la puntuación más alta es el ganador, incluso si no tienen 40 puntos o más.

Si todos los bots rodaran hasta que se rompieran, ¡todos tendrían un puntaje de cero al final de la ronda 200 y todos ganarían! Por lo tanto, la estrategia del Enjambre Cooperativo es cooperar siempre que todos los jugadores tengan un puntaje de cero, pero jugar normalmente si alguien anota algún punto.

En esta publicación, envío dos bots: el primero es CooperativeSwarmBot y el segundo es CooperativeThrowTwice. CooperativeSwarmBot sirve como una clase base para todos los bots que son formalmente parte del enjambre cooperativo, y tiene un comportamiento de marcador de posición de simplemente aceptar su primer lanzamiento exitoso cuando falla la cooperación. CooperativeSwarmBot tiene CooperativeSwarmBot como padre y es idéntico en todos los aspectos, excepto que su comportamiento no cooperativo es hacer dos rollos en lugar de uno. En los próximos días revisaré esta publicación para agregar nuevos bots que usen comportamientos mucho más inteligentes que juegan contra bots no cooperativos.

Código

class CooperativeSwarmBot(Bot):
    def defection_strategy(self, scores, last_round):
        yield False

    def make_throw(self, scores, last_round):
        cooperate = max(scores) == 0
        if (cooperate):
            while True:
                yield True
        else:
            yield from self.defection_strategy(scores, last_round)

class CooperativeThrowTwice(CooperativeSwarmBot):
    def defection_strategy(self, scores, last_round):
        yield True
        yield False

Análisis

Viabilidad

Es muy difícil cooperar en este juego porque necesitamos el apoyo de los ocho jugadores para que funcione. Dado que cada clase de bot está limitada a una instancia por juego, este es un objetivo difícil de lograr. Por ejemplo, las probabilidades de elegir ocho bots cooperativos de un grupo de 100 bots cooperativos y 30 bots no cooperativos son:

100130991299812897127961269512594124931230.115

En términos más generales, las probabilidades de elegir bots cooperativos de un grupo de bots cooperativos bots no cooperativos son:icn

c!÷(ci)!(c+n)!÷(c+ni)!

A partir de esta ecuación, podemos mostrar fácilmente que necesitaríamos alrededor de 430 bots cooperativos para que el 50% de los juegos terminen cooperativamente, o alrededor de 2900 bots para el 90% (usando según las reglas, ).i=8n=38

Caso de estudio

Por varias razones (vea las notas al pie 1 y 2), un enjambre cooperativo adecuado nunca competirá en los juegos oficiales. Como tal, resumiré los resultados de una de mis propias simulaciones en esta sección.

Esta simulación ejecutó 10000 juegos usando los otros 38 bots que habían sido publicados aquí la última vez que revisé y 2900 bots que tenían CooperativeSwarmBot como su clase principal. El controlador informó que 9051 de los 10000 juegos (90.51%) terminaron en 200 rondas, lo que está bastante cerca de la predicción de que el 90% de los juegos serían cooperativos. La implementación de estos bots fue trivial; aparte de CooperativeSwarmBot, todos tomaron esta forma:

class CooperativeSwarm_1234(CooperativeSwarmBot):
    pass

Menos del 3% de los bots tenían un porcentaje de victorias inferior al 80%, y poco más del 11% de los bots ganaron cada juego que jugaron. La mediana del porcentaje de victorias de los 2900 bots en el enjambre es de alrededor del 86%, lo cual es escandalosamente bueno. En comparación, los mejores jugadores en la clasificación oficial actual ganan menos del 22% de sus juegos. No puedo ajustar la lista completa del enjambre cooperativo dentro de la longitud máxima permitida para una respuesta, por lo que si desea ver eso, tendrá que ir aquí: https://pastebin.com/3Zc8m1Ex

Dado que cada bot jugó en un promedio de aproximadamente 27 juegos, la suerte juega un rol relativamente grande cuando observa los resultados de los bots individuales. Como todavía no he implementado una estrategia avanzada para juegos no cooperativos, la mayoría de los otros bots se beneficiaron drásticamente al jugar contra el enjambre cooperativo, realizando incluso la tasa de ganancia media del enjambre cooperativo del 86%.

Los resultados completos de los bots que no están en el enjambre se enumeran a continuación; Hay dos bots cuyos resultados creo que merecen especial atención. Primero, StopBot no pudo ganar ningún juego. Esto es particularmente trágico porque el enjambre cooperativo estaba usando exactamente la misma estrategia que StopBot; habrías esperado que StopBot ganara ocho de sus juegos por casualidad, y un poco más porque el enjambre cooperativo se ve obligado a dar a sus oponentes el primer movimiento. Sin embargo, el segundo resultado interesante es que el arduo trabajo de PointsAreForNerdsBot finalmente dio sus frutos: ¡cooperó con el enjambre y logró ganar todos los juegos que jugó!

+---------------------+----+--------+--------+------+------+-------+------+--------+
|Bot                  |Win%|    Wins|  Played|   Max|   Avg|Avg win|Throws|Success%|
+---------------------+----+--------+--------+------+------+-------+------+--------+
|AggressiveStalker    |100.0|      21|      21|    42| 40.71|  40.71|  3.48|   46.32|
|PointsAreForNerdsBot |100.0|      31|      31|     0|  0.00|   0.00|  6.02|    0.00|
|TakeFive             |100.0|      18|      18|    44| 41.94|  41.94|  2.61|   50.93|
|Hesitate             |100.0|      26|      26|    44| 41.27|  41.27|  3.32|   41.89|
|Crush                |100.0|      34|      34|    44| 41.15|  41.15|  5.38|    6.73|
|StepBot              |97.0|      32|      33|    46| 41.15|  42.44|  4.51|   24.54|
|LastRound            |96.8|      30|      31|    44| 40.32|  41.17|  3.54|   45.05|
|Chaser               |96.8|      30|      31|    47| 42.90|  44.33|  3.04|   52.16|
|GoHomeBot            |96.8|      30|      31|    44| 40.32|  41.67|  5.60|    9.71|
|Stalker              |96.4|      27|      28|    44| 41.18|  41.44|  2.88|   57.53|
|ClunkyChicken        |96.2|      25|      26|    44| 40.96|  41.88|  2.32|   61.23|
|AdaptiveRoller       |96.0|      24|      25|    44| 39.32|  40.96|  4.49|   27.43|
|GoTo20Bot            |95.5|      21|      22|    44| 40.36|  41.33|  4.60|   30.50|
|FortyTeen            |95.0|      19|      20|    48| 44.15|  45.68|  3.71|   43.97|
|BinaryBot            |94.3|      33|      35|    44| 41.29|  41.42|  2.87|   53.07|
|EnsureLead           |93.8|      15|      16|    55| 42.56|  42.60|  4.04|   26.61|
|Roll6Timesv2         |92.9|      26|      28|    45| 40.71|  42.27|  4.07|   29.63|
|BringMyOwn_dice      |92.1|      35|      38|    44| 40.32|  41.17|  4.09|   28.40|
|LizduadacBot         |92.0|      23|      25|    54| 47.32|  51.43|  5.70|    5.18|
|FooBot               |91.7|      22|      24|    44| 39.67|  41.45|  3.68|   51.80|
|Alpha                |91.7|      33|      36|    48| 38.89|  42.42|  2.16|   65.34|
|QuotaBot             |90.5|      19|      21|    53| 38.38|  42.42|  3.88|   24.65|
|GoBigEarly           |88.5|      23|      26|    47| 41.35|  42.87|  3.33|   46.38|
|ExpectationsBot      |88.0|      22|      25|    44| 39.08|  41.55|  3.57|   45.34|
|LeadBy5Bot           |87.5|      21|      24|    50| 37.46|  42.81|  2.20|   63.88|
|GamblersFallacy      |86.4|      19|      22|    44| 41.32|  41.58|  2.05|   63.11|
|BePrepared           |86.4|      19|      22|    59| 39.59|  44.79|  3.81|   35.96|
|RollForLuckBot       |85.7|      18|      21|    54| 41.95|  47.67|  4.68|   25.29|
|OneStepAheadBot      |84.6|      22|      26|    50| 41.35|  46.00|  3.34|   42.97|
|FlipCoinRollDice     |78.3|      18|      23|    51| 37.61|  44.72|  1.67|   75.42|
|BlessRNG             |77.8|      28|      36|    47| 40.69|  41.89|  1.43|   83.66|
|FutureBot            |77.4|      24|      31|    49| 40.16|  44.38|  2.41|   63.99|
|SlowStart            |68.4|      26|      38|    57| 38.53|  45.31|  1.99|   66.15|
|NotTooFarBehindBot   |66.7|      20|      30|    50| 37.27|  42.00|  1.29|   77.61|
|ThrowThriceBot       |63.0|      17|      27|    51| 39.63|  44.76|  2.50|   55.67|
|OneInFiveBot         |58.3|      14|      24|    54| 33.54|  44.86|  2.91|   50.19|
|MatchLeaderBot       |48.1|      13|      27|    49| 40.15|  44.15|  1.22|   82.26|
|StopBot              | 0.0|       0|      27|    43| 30.26|   0.00|  1.00|   82.77|
+---------------------+----+--------+--------+------+------+-------+------+--------+

Defectos

Hay un par de inconvenientes en este enfoque cooperativo. Primero, cuando juegan contra bots no cooperativos, los bots cooperativos nunca obtienen la ventaja del primer turno porque cuando juegan primero, todavía no saben si sus oponentes están dispuestos a cooperar y, por lo tanto, no tienen más remedio que obtener un puntaje de cero. Del mismo modo, esta estrategia cooperativa es extremadamente vulnerable a la explotación por parte de robots maliciosos; Por ejemplo, durante el juego cooperativo, el bot que juega el último en la última ronda puede optar por dejar de tirar inmediatamente para que todos los demás pierdan (suponiendo, por supuesto, que su primer lanzamiento no fue un seis).

Al cooperar, todos los bots pueden lograr la solución óptima de una tasa de ganancia del 100%. Como tal, si la tasa de ganancia fuera lo único que importaba, entonces la cooperación sería un equilibrio estable y no habría nada de qué preocuparse. Sin embargo, algunos bots pueden priorizar otros objetivos, como llegar a la cima de la clasificación. Esto significa que existe el riesgo de que otro bot falle después de tu último turno, lo que crea un incentivo para que lo deseches primero. Debido a que la configuración de esta competencia no nos permite ver lo que hicieron nuestros oponentes en sus juegos anteriores, no podemos penalizar a las personas que desertaron. Por lo tanto, la cooperación es, en última instancia, un equilibrio inestable condenado al fracaso.

Notas al pie

[1]: Las razones principales por las que no quiero enviar miles de bots en lugar de solo dos son que hacerlo retrasaría la simulación por un factor del orden de 1000 [2], y que hacerlo afectaría significativamente ganar porcentajes ya que otros bots estarían jugando casi exclusivamente contra el enjambre en lugar de entre sí. Sin embargo, lo más importante es el hecho de que, incluso si quisiera, no sería capaz de hacer tantos bots en un plazo razonable sin romper el espíritu de la regla de que "un bot no debe implementar exactamente la misma estrategia que un existente, intencional o accidentalmente ".

[2]: Creo que hay dos razones principales por las que la simulación se ralentiza cuando se ejecuta un enjambre cooperativo. Primero, más bots significan más juegos si quieres que cada bot juegue en la misma cantidad de juegos (en el caso de estudio, la cantidad de juegos diferirá en un factor de aproximadamente 77). Segundo, los juegos cooperativos solo toman más tiempo porque duran 200 rondas completas, y dentro de una ronda los jugadores tienen que seguir rodando indefinidamente. Para mi configuración, los juegos tardaron 40 veces más en simularse: el estudio de caso tardó un poco más de tres minutos en ejecutar 10000 juegos, pero después de eliminar el enjambre cooperativo terminaría 10000 juegos en solo 4.5 segundos. Entre estas dos razones, calculo que tomaría aproximadamente 3100 veces más tiempo medir con precisión el rendimiento de los bots cuando hay un enjambre que compite en comparación con cuando no lo hay.

Einhaender
fuente
44
Guau. Y bienvenidos a PPCG. Esta es la primera respuesta. Realmente no estaba planeando una situación como esta. Ciertamente encontraste un vacío en las reglas. No estoy realmente seguro de cómo debo calificar esto, ya que su respuesta es una colección de bots en lugar de un solo bot. Sin embargo, lo único que diré ahora es que se siente injusto que un participante controle el 98.7% de todos los bots.
maxb
2
En realidad no quiero que haya bots duplicados en la competencia oficial; es por eso que ejecuté la simulación yo mismo en lugar de enviar miles de bots casi idénticos. Revisaré mi presentación para que quede más claro.
Einhaender
1
Si hubiera anticipado una respuesta como esta, habría cambiado los juegos que van a 200 rondas para que no den puntajes a los jugadores. Sin embargo, como observa, hay una regla sobre la creación de bots idénticos que haría que esta estrategia sea contraria a las reglas. No voy a cambiar las reglas, ya que sería injusto para todos los que han hecho un bot. Sin embargo, el concepto de cooperación es muy interesante, y espero que se presenten otros bots que implementen la estrategia de cooperación en combinación con su propia estrategia única.
maxb
1
Creo que tu publicación es clara después de leerla más a fondo.
maxb
¿Cuántos bots existentes necesitarían envolver su código en este marco de cooperación para que la mayoría de ellos vea una ganancia neta en su ubicación en la tabla de clasificación? Mi ingenua suposición es del 50%.
Sparr
10

GoTo20Bot

class GoTo20Bot(Bot):

    def make_throw(self, scores, last_round):
        target = min(20, 40 - scores[self.index])
        if last_round:
            target = max(scores) - scores[self.index] + 1
        while sum(self.current_throws) < target:
            yield True
        yield False

Solo inténtalo con todos GoToNBot, y 20, 22, 24 juega mejor. No se porque.


Actualización: siempre deja de lanzar si obtienes un puntaje de 40 o más.

tsh
fuente
También he experimentado con ese tipo de bots. El puntaje promedio más alto por ronda se encuentra cuando el bot llega a 16, pero supongo que el "juego final" hace que el bot de 20 gane con más frecuencia.
maxb
@maxb No es así, 20 seguirá siendo el mejor sin el "juego final" en mi prueba. Tal vez lo había probado en la versión anterior del controlador.
tsh
Realicé una prueba por separado antes de diseñar este desafío, donde calculé el puntaje promedio por ronda para las dos tácticas en mi publicación ("lanzar x veces" y "lanzar hasta x puntaje"), y el máximo que encontré fue de 15-16 . Aunque el tamaño de mi muestra podría haber sido demasiado pequeño, noté inestabilidad.
maxb
2
He hecho algunas pruebas con esto, y mi conclusión es simplemente que 20 funciona bien porque es 40/2. Aunque no estoy completamente seguro. Cuando configuré end_score4000 (y cambié su bot para usar esto en el targetcálculo), los 15-16 bots fueron mucho mejores. Pero si el juego fuera solo para aumentar tu puntaje, sería trivial.
maxb
1
@maxb Si end_scorees 4000, es casi imposible obtener 4000 antes de 200 turnos. Y el juego es simplemente quién obtuvo la puntuación más alta en 200 turnos. Y detenerse en 15 debería funcionar, ya que esta vez la estrategia para la puntuación más alta en un turno es igual a la puntuación más alta en 200 turnos.
tsh
10

Rodillo Adaptativo

Comienza más agresivo y se calma hacia el final de la ronda.
Si cree que está ganando, tira un tiempo extra por seguridad.

class AdaptiveRoller(Bot):

    def make_throw(self, scores, last_round):
        lim = min(self.end_score - scores[self.index], 22)
        while sum(self.current_throws) < lim:
            yield True
        if max(scores) == scores[self.index] and max(scores) >= self.end_score:
            yield True
        while last_round and scores[self.index] + sum(self.current_throws) <= max(scores):
            yield True
        yield False
Emigna
fuente
Gran primera presentación! Lo ejecutaré contra mis bots que escribí para probar, pero actualizaré la puntuación más alta cuando se hayan publicado más bots.
maxb
Ejecuté algunas pruebas con ligeras modificaciones a su bot. lim = max(min(self.end_score - scores[self.index], 24), 6)elevar el máximo a 24 y agregar un mínimo de 6 aumentan el porcentaje ganador por sí mismos y aún más combinados.
AKroell
@AKroell: ¡Genial! Tengo la intención de hacer algo similar para asegurarme de que ruede varias veces al final, pero aún no me he tomado el tiempo para hacerlo. Aunque parezca extraño, parece funcionar peor con esos valores cuando hago 100k carreras. Sin embargo, solo he probado con 18 bots. Tal vez debería hacer algunas pruebas con todos los bots.
Emigna
5

Alfa

class Alpha(Bot):
    def make_throw(self, scores, last_round):
        # Throw until we're the best.
        while scores[self.index] + sum(self.current_throws) <= max(scores):
            yield True

        # Throw once more to assert dominance.
        yield True
        yield False

Alpha se niega a ser el segundo de nadie. Mientras haya un bot con una puntuación más alta, seguirá rodando.

Mnemotécnico
fuente
Debido a cómo yieldfunciona, si comienza a rodar, nunca se detendrá. Querrás actualizar my_scoreen el ciclo.
Spitemaster
@Spitemaster fijo, gracias.
Mnemónico
5

NotTooFarBehindBot

class NotTooFarBehindBot(Bot):
    def make_throw(self, scores, last_round):
        while True:
            current_score = scores[self.index] + sum(self.current_throws)
            number_of_bots_ahead = sum(1 for x in scores if x > current_score)
            if number_of_bots_ahead > 1:
                yield True
                continue
            if number_of_bots_ahead != 0 and last_round:
                yield True
                continue
            break
        yield False

La idea es que otros bots pueden perder puntos, por lo que estar en segundo lugar no es malo, pero si estás muy atrasado, también podrías ir a la quiebra.

Stuart Moore
fuente
1
Bienvenido a PPCG! Estoy revisando tu presentación, y parece que cuantos más jugadores estén en el juego, menor será el porcentaje de victorias para tu bot. No puedo decir por qué de inmediato. Con los bots igualados 1 vs1, obtienes un 10% de winrate La idea suena prometedora, y el código parece correcto, por lo que realmente no puedo decir por qué su tasa de ganancia no es mayor.
maxb
66
He mirado en el comportamiento, y esta línea me había confundido: 6: Bot NotTooFarBehindBot plays [4, 2, 4, 2, 3, 3, 5, 5, 1, 4, 1, 4, 2, 4, 3, 6] with scores [0, 9, 0, 20, 0, 0, 0] and last round == False. A pesar de que su bot está a la cabeza después de 7 lanzamientos, continúa hasta que alcanza un 6. ¡Mientras escribo esto, descubrí el problema! El scoresúnico contiene los puntajes totales, no los casos de dado para la ronda actual. Deberías modificarlo para que sea current_score = scores[self.index] + sum(self.current_throws).
maxb
Gracias, hará ese cambio!
Stuart Moore
5

GoHomeBot

class GoHomeBot(Bot):
    def make_throw(self, scores, last_round):
        while scores[self.index] + sum(self.current_throws) < 40:
            yield True
        yield False

Queremos ir a lo grande o ir a casa, ¿verdad? GoHomeBot en su mayoría solo se va a casa. (¡Pero sorprendentemente bien!)

Spitemaster
fuente
Dado que este bot siempre va por 40 puntos, nunca tendrá ningún punto en la scoreslista. Había un bot como este antes (el bot GoToEnd), pero David eliminó su respuesta. Reemplazaré ese bot por el tuyo.
maxb
1
Es bastante divertido ver las estadísticas ampliadas de estos bots: a excepción de pointsAreForNerds y StopBot, este bot tiene los puntos promedio más bajos y, sin embargo, tiene una buena proporción de victorias
Belhenix
5

Asegúrese de llevar

class EnsureLead(Bot):

    def make_throw(self, scores, last_round):
        otherScores = scores[self.index+1:] + scores[:self.index]
        maxOtherScore = max(otherScores)
        maxOthersToCome = 0
        for i in otherScores:
            if (i >= 40): break
            else: maxOthersToCome = max(maxOthersToCome, i)
        while True:
            currentScore = sum(self.current_throws)
            totalScore = scores[self.index] + currentScore
            if not last_round:
                if totalScore >= 40:
                    if totalScore < maxOtherScore + 10:
                        yield True
                    else:
                        yield False
                elif currentScore < 20:
                    yield True
                else:
                    yield False
            else:
                if totalScore < maxOtherScore + 1:
                    yield True
                elif totalScore < maxOthersToCome + 10:
                    yield True
                else:
                    yield False

AsegúreseLead toma prestadas ideas de GoTo20Bot. Agrega el concepto que siempre considera (cuando está en last_round o llegando a 40) que hay otros que tendrán al menos un rollo más. Por lo tanto, el bot intenta adelantarse un poco, de modo que tienen que ponerse al día.

Dirk Herrmann
fuente
4

Roll6TimesV2

No supera el mejor actual, pero creo que será mucho mejor con más bots en juego.

class Roll6Timesv2(Bot):
    def make_throw(self, scores, last_round):

        if not last_round:
            i = 0
            maximum=6
            while ((i<maximum) and sum(self.current_throws)+scores[self.index]<=40 ):
                yield True
                i=i+1

        if last_round:
            while scores[self.index] + sum(self.current_throws) < max(scores):
                yield True
        yield False

Juego realmente impresionante por cierto.

itsmephil12345
fuente
Bienvenido a PPCG! Muy impresionante no solo para tu primer desafío KotH, sino también para tu primera respuesta. Me alegro de que te haya gustado el juego! He discutido mucho sobre la mejor táctica para el juego después de la noche en que lo jugué, así que parecía perfecto para un desafío. Actualmente estás en el tercer lugar de 18.
maxb
4

StopBot

class StopBot(Bot):
    def make_throw(self, scores, last_round):
        yield False

Literalmente solo un tiro.

Esto es equivalente a la Botclase base .

Zacharý
fuente
1
No lo lamentes Estás siguiendo todas las reglas, aunque me temo que tu bot no es terriblemente efectivo con un promedio de 2.5 puntos por ronda.
maxb
1
Lo sé, aunque alguien tuvo que publicar ese bot. Bots degenerados por la pérdida.
Zacharý
55
Yo diría que estoy impresionado por tu bot asegurando exactamente una victoria en la última simulación, demostrando que no es completamente inútil.
maxb
2
¡¿GANÓ UN JUEGO ?! Eso es sorprendente
Zacharý
3

BringMyOwn_dice (BMO_d)

Este bot ama los dados, trae 2 (parece que realiza el mejor) dado propio. Antes de lanzar dados en una ronda, lanza sus propios 2 dados y calcula su suma, esta es la cantidad de lanzamientos que realizará, solo lanza si aún no tiene 40 puntos.

class BringMyOwn_dice(Bot):

    def __init__(self, *args):
        import random as rnd
        self.die = lambda: rnd.randint(1,6)
        super().__init__(*args)

    def make_throw(self, scores, last_round):

        nfaces = self.die() + self.die()

        s = scores[self.index]
        max_scores = max(scores)

        for _ in range(nfaces):
            if s + sum(self.current_throws) > 39:
                break
            yield True

        yield False
ბიმო
fuente
2
Estaba pensando en un bot aleatorio que usa un lanzamiento de moneda, ¡pero esto está más en espíritu con el desafío! Creo que dos dados rinden mejor, ya que obtienes la mayor cantidad de puntos por ronda cuando lanzas el dado 5-6 veces, cerca del puntaje promedio cuando lanzas dos dados.
maxb
3

FooBot

class FooBot(Bot):
    def make_throw(self, scores, last_round):
        max_score = max(scores)

        while True:
            round_score = sum(self.current_throws)
            my_score = scores[self.index] + round_score

            if last_round:
                if my_score >= max_score:
                    break
            else:
                if my_score >= self.end_score or round_score >= 16:
                    break

            yield True

        yield False
Peter Taylor
fuente
# Must throw at least onceno es necesario: se lanza una vez antes de llamar a su bot. Tu bot siempre lanzará un mínimo de dos veces.
Spitemaster
Gracias. Fui engañado por el nombre del método.
Peter Taylor
@PeterTaylor ¡Gracias por su envío! Puse el nombre del make_throwmétodo desde el principio, cuando quería que los jugadores pudieran saltar su turno. Supongo que sería un nombre más apropiado keep_throwing. Gracias por los comentarios en la caja de arena, ¡realmente ayudó a hacer de este un desafío adecuado!
maxb
3

Ir a lo grande temprano

class GoBigEarly(Bot):
    def make_throw(self, scores, last_round):
        yield True  # always do a 2nd roll
        while scores[self.index] + sum(self.current_throws) < 25:
            yield True
        yield False

Concepto: Intenta ganar en grande en una tirada temprana (llegando a 25) y luego avanza lentamente desde allí 2 tiradas a la vez.

Stuart Moore
fuente
3

BinaryBot

Intenta acercarse al puntaje final, de modo que tan pronto como alguien más active la última ronda, pueda superar su puntaje para la victoria. El objetivo siempre está a medio camino entre el puntaje actual y el puntaje final.

class BinaryBot(Bot):

    def make_throw(self, scores, last_round):
        target = (self.end_score + scores[self.index]) / 2
        if last_round:
            target = max(scores)

        while scores[self.index] + sum(self.current_throws) < target:
            yield True

        yield False
Caín
fuente
Interesante, Hesitatetambién se niega a cruzar la línea primero. Necesita rodear su función con las classcosas.
Christian Sievers
3

PointsAreForNerdsBot

class PointsAreForNerdsBot(Bot):
    def make_throw(self, scores, last_round):
        while True:
            yield True

Este no necesita explicación.

OneInFiveBot

class OneInFiveBot(Bot):
    def make_throw(self, scores, last_round):
        while random.randint(1,5) < 5:
            yield True
        yield False

Sigue rodando hasta que saca un cinco en su propio dado de 5 lados. ¡Cinco es menos de seis, así que TIENE QUE GANAR!

The_Bob
fuente
2
Bienvenido a PPCG! Estoy seguro de que lo sabes, ¡pero tu primer bot es literalmente el peor bot de esta competencia! El OneInFiveBotes una idea ingeniosa, pero creo que sufre en el final del juego en comparación con algunos de los robots más avanzados. Sigue siendo una gran presentación!
maxb
2
el OneInFiveBotes bastante interesante en la forma en que constantemente tiene la puntuación más alta alcanzada.
AKroell
1
Gracias por dar StopBotun saco de boxeo: P. El OneInFiveBot en realidad es bastante bueno, ¡buen trabajo!
Zacharý
@maxb Sí, ahí es donde obtuve el nombre. Sinceramente, no hice la prueba OneInFiveBoty me está yendo mucho mejor de lo que esperaba
The_Bob
3

LizduadacBot

Intenta ganar en 1 paso. La condición final es algo arbitraria.

Esta es también mi primera publicación (y soy nuevo en Python), así que si venciera a "PointsAreForNerdsBot", ¡estaría feliz!

class LizduadacBot(Bot):

    def make_throw(self, scores, last_round):
        while scores[self.index] + sum(self.current_throws) < 50 or scores[self.index] + sum(self.current_throws) < max(scores):
            yield True
        yield False
lizduadac
fuente
¡Bienvenido a PPCG (y bienvenido a Python)! Te costará mucho perder contra PointsAreForNerdsBot, pero a tu bot realmente le va bastante bien. Actualizaré el puntaje más tarde esta noche o mañana, pero su tasa de ganancia es de aproximadamente 15%, que es más alta que el promedio de 12.5%.
maxb
Por "tiempo difícil", quieren decir que es imposible (a menos que haya entendido mal)
Zacharý
@maxb ¡Realmente no pensé que la tasa de ganancias sería tan alta! (No lo probé localmente). Me pregunto si cambiar los 50 para que sea un poco más alto / más bajo aumentaría la tasa de ganancias.
lizduadac
3

Comienzo lento

Este bot implementa el algoritmo TCP Slow Start. Ajusta su número de lanzamientos ( ni ) de acuerdo con su turno anterior: si no sacó un 6 en el turno anterior, aumenta el ni para este turno; mientras que se reduce ni si lo hizo.

class SlowStart(Bot):
    def __init__(self, *args):
        super().__init__(*args)
        self.completeLastRound = False
        self.nor = 1
        self.threshold = 8

    def updateValues(self):
        if self.completeLastRound:
            if self.nor < self.threshold:
                self.nor *= 2
            else:
                self.nor += 1
        else:
            self.threshold = self.nor // 2
            self.nor = 1


    def make_throw(self, scores, last_round):

        self.updateValues()
        self.completeLastRound = False

        i = 1
        while i < self.nor:
            yield True

        self.completeLastRound = True        
        yield False
bastante sencillo
fuente
Bienvenido a PPCG! Enfoque interesante, no sé cuán sensible es a las fluctuaciones aleatorias. Dos cosas que se necesitan para hacer esta ejecución: def updateValues():deberían ser def updateValues(self):(o def update_values(self):si desea seguir PEP8). En segundo lugar, la llamada updateValues()debería ser self.updateValues()(o self.update_vales()).
maxb
2
Además, creo que necesita actualizar su ivariable en el ciclo while. En este momento, su bot pasa el ciclo while por completo o está atascado en el ciclo while hasta que
llegue a
En la puntuación más alta actual, me tomé la libertad de implementar estos cambios. Creo que podrías experimentar con el valor inicial self.nory ver cómo afecta el rendimiento de tu bot.
maxb
3

KwisatzHaderach

import itertools
class KwisatzHaderach(Bot):
    """
    The Kwisatz Haderach foresees the time until the coming
    of Shai-Hulud, and yields True until it is immanent.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.roller = random.Random()
        self.roll = lambda: self.roller.randint(1, 6)
        self.ShaiHulud = 6

    def wormsign(self):
        self.roller.setstate(random.getstate())
        for i in itertools.count(0):
            if self.roll() == self.ShaiHulud:
                return i

    def make_throw(self, scores, last_round):
        target = max(scores) if last_round else self.end_score
        while True:
            for _ in range(self.wormsign()):
                yield True
            if sum(self.current_throws) > target + random.randint(1, 6):
                yield False                                               

La presciencia generalmente gana, pero el destino no siempre se puede evitar.
¡Grandes y misteriosos son los caminos de Shai-Hulud!


En los primeros días de este desafío (es decir, antes NeoBotse publicó), escribí un Oraclebot casi trivial :

    class Oracle(Bot):
        def make_throw(self, scores, last_round):
        randơm = random.Random()
        randơm.setstate(random.getstate())
        while True:
            yield randơm.randint(1, 6) != 6

pero no lo publiqué porque no pensé que fuera lo suficientemente interesante;) Pero una vez NeoBot tomé la delantera, comencé a pensar en cómo vencer su capacidad perfecta para predecir el futuro. Así que aquí hay una cita de Dune; es cuando Paul Atreides, el Kwisatz Haderach, se encuentra en un nexo desde el cual se puede desenrollar una infinidad de futuros diferentes:

La presciencia, se dio cuenta, era una iluminación que incorporaba los límites de lo que revelaba, a la vez una fuente de precisión y error significativo. Intervino una especie de indeterminación de Heisenberg: el gasto de energía que reveló lo que vio, cambió lo que vio ... ... la acción más minuciosa: un abrir y cerrar de ojos, una palabra descuidada, un grano de arena fuera de lugar, movió una palanca gigantesca a través del universo conocido Vio violencia con el resultado sujeto a tantas variables que su movimiento más leve creó grandes cambios en los patrones.

La visión lo hizo querer congelarse en la inmovilidad, pero esto también fue acción con sus consecuencias.

Así que aquí estaba la respuesta: prever el futuro es cambiarlo; y si tiene mucho cuidado, entonces, por acción selectiva o inacción, puede cambiarlo de manera ventajosa, al menos la mayor parte del tiempo. ¡Incluso KwisatzHaderachno pueden obtener una tasa de ganancia del 100%!

Dani O
fuente
Parece que este bot cambia el estado del generador de números aleatorios, para asegurarse de que evita lanzar 6, o al menos lo anticipa. Lo mismo ocurre con HarkonnenBot. Sin embargo, noto que la tasa de victorias de estos bots es mucho más alta que la de NeoBot. ¿Estás manipulando activamente el generador de números aleatorios para evitar que salga 6?
maxb
¡Oh, en mi primera lectura no me di cuenta de que esto no solo es mejor NeoBotsino también mejor! También me gusta cómo das un ejemplo de lo que debería hacer todo lo que usa la aleatoriedad (especialmente el controlador) aquí: usa tu propia random.Randominstancia. Como NeoBot, esto parece un poco sensible a los cambios de detalles de implementación no especificados del controlador.
Christian Sievers
@maxb: HarkonnenBotno toca el RNG; no le importan en absoluto los números aleatorios. Simplemente envenena a todos los demás bots, luego camina hasta la línea de meta lo más lentamente posible. Como muchas delicias culinarias, la venganza es un plato que se saborea mejor lentamente, después de una preparación larga y delicada.
Dani O
@ChristianSievers: a diferencia de NeoBot (y HarkonnenBot), se KwisatzHaderachbasa en un solo detalle de la implementación; en particular, no necesita saber cómo se implementa random.random (), solo que el controlador lo usa; D
Dani O
1
He revisado todos tus bots. He decidido tratar KwisatzHaderachy HarkonnenBotdel mismo modo que NeoBot. Recibirán sus puntajes de una simulación con menos juegos, y no estarán en la simulación oficial. Sin embargo, terminarán en la lista de puntajes muy parecidos NeoBot. La razón principal para que no estén en la simulación oficial es que arruinarán otras estrategias de bot. Sin embargo. WisdomOfCrowdsdebería ser adecuado para la participación, y tengo curiosidad por los nuevos cambios que ha realizado.
maxb
2
class ThrowThriceBot(Bot):

    def make_throw(self, scores, last_round):
        yield True
        yield True
        yield False 

Bueno, eso es obvio.

michi7x7
fuente
He hecho algunos experimentos con esa clase de bots (fue la táctica que usé cuando jugué por primera vez). Fui con 4 tiros entonces, aunque 5-6 tienen un puntaje promedio más alto por ronda.
maxb
Además, ¡felicidades por tu primera respuesta KotH!
maxb
2
class LastRound(Bot):
    def make_throw(self, scores, last_round):
        while sum(self.current_throws) < 15 and not last_round and scores[self.index] + sum(self.current_throws) < 40:
            yield True
        while max(scores) > scores[self.index] + sum(self.current_throws):
            yield True
        yield False

LastRound actúa como si siempre fuera la última ronda y es el último bot: sigue rodando hasta que está a la cabeza. Tampoco quiere conformarse con menos de 15 puntos a menos que en realidad sea la última ronda o llegue a 40 puntos.

Spitemaster
fuente
Enfoque interesante Creo que tu bot sufre si comienza a quedarse atrás. Dado que las probabilidades de obtener> 30 puntos en una sola ronda son bajas, es más probable que su bot se mantenga en su puntaje actual.
maxb
1
Sospecho que esto sufre el mismo error que cometí (vea los comentarios de NotTooFarBehindBot), como en la última ronda, si no está ganando, seguirá lanzando hasta que obtenga un 6 (las puntuaciones [self.index] nunca se actualizan) En realidad: ¿Tienes esa desigualdad de la manera incorrecta? max (score) siempre será> = score [self.index]
Stuart Moore
@StuartMoore Jaja, sí, creo que tienes razón. ¡Gracias!
Spitemaster
Sospecho que quieres "y last_round" en el segundo tiempo para hacer lo que quieras; de lo contrario, el segundo tiempo se usará si last_round es verdadero o no
Stuart Moore
3
Eso es intencional Siempre trata de estar a la cabeza cuando termina su turno.
Spitemaster
2

CuotaBot

Implementé un ingenuo sistema de "cuotas", que en realidad parecía tener un puntaje bastante alto en general.

class QuotaBot(Bot):
    def __init__(self, *args):
        super().__init__(*args)
        self.quota = 20
        self.minquota = 15
        self.maxquota = 35

    def make_throw(self, scores, last_round):
        # Reduce quota if ahead, increase if behind
        mean = sum(scores) / len(scores)
        own_score = scores[self.index]

        if own_score < mean - 5:
            self.quota += 1.5
        if own_score > mean + 5:
            self.quota -= 1.5

        self.quota = max(min(self.quota, self.maxquota), self.minquota)

        if last_round:
            self.quota = max(scores) - own_score + 1

        while sum(self.current_throws) < self.quota:
            yield True

        yield False

FlipTack
fuente
if own_score mean + 5:me da un error Tambiénwhile sum(self.current_throws)
Spitemaster
@Spitemaster fue un error al pegar en el intercambio de pila, debería funcionar ahora.
FlipTack
@Spitemaster es porque hubo <y >símbolos que interferían con las <pre>etiquetas que estaba usando
FlipTack
2

ExpectativasBot

Simplemente juega directamente, calcula el valor esperado para el lanzamiento de dados y solo lo hace si es positivo.

class ExpectationsBot(Bot):

    def make_throw(self, scores, last_round):
        #Positive average gain is 2.5, is the chance of loss greater than that?
        costOf6 = sum(self.current_throws) if scores[self.index] + sum(self.current_throws) < 40  else scores[self.index] + sum(self.current_throws)
        while 2.5 > (costOf6 / 6.0):
            yield True
            costOf6 = sum(self.current_throws) if scores[self.index] + sum(self.current_throws) < 40  else scores[self.index] + sum(self.current_throws)
        yield False

Estaba teniendo problemas para ejecutar el controlador, obtuve un "NameError: el nombre 'bots_per_game' no está definido" en el multiproceso, así que realmente no tengo idea de cómo funciona.

Caín
fuente
1
Creo que esto termina siendo equivalente a un bot "Ir a 16", pero todavía no tenemos uno de ellos
Stuart Moore
1
@StuartMoore Eso ... es un punto muy cierto, sí
Caín
Me encontré con sus problemas con el controlador cuando lo ejecuté en mi máquina Windows. De alguna manera funcionó bien en mi máquina Linux. Estoy actualizando el controlador y actualizaré la publicación una vez que haya terminado.
maxb
@maxb Gracias, probablemente algo sobre qué variables están disponibles en los diferentes procesos. FYI también actualizó esto, cometí un error tonto al ceder: /
Caín
2

BlessRNG

class BlessRNG(Bot):
    def make_throw(self, scores, last_round):
        if random.randint(1,2) == 1 :
            yield True
        yield False

BlessRNG FrankerZ GabeN BlessRNG

Dados Mastah
fuente
2

Cuarenta Adolescentes

class FortyTeen(Bot):
    def make_throw(self, scores, last_round):
        if last_round:
            max_projected_score = max([score+14 if score<self.end_score else score for score in scores])
            target = max_projected_score - scores[self.index]
        else:
            target = 14

        while sum(self.current_throws) < target:
            yield True
        yield False

Intenta 14 puntos hasta la última ronda, luego asume que todos los demás intentarán 14 puntos e intentarán empatar ese puntaje.

histocrat
fuente
Tengo TypeError: unsupported operand type(s) for -: 'list' and 'int'con tu bot.
tsh
Supongo que max_projected_scoredebería ser el máximo de la lista en lugar de la lista completa, ¿estoy en lo cierto? De lo contrario, tengo el mismo problema que tsh.
maxb
Vaya, editado para arreglar.
histocrat
2

Vacilar

Hace dos pasos modestos, luego espera a que alguien más cruce la línea. La versión actualizada ya no intenta superar la puntuación más alta, solo quiere alcanzarla, ¡mejorando el rendimiento eliminando dos bytes del código fuente!

class Hesitate(Bot):
    def make_throw(self, scores, last_round):
        myscore = scores[self.index]
        if last_round:
            target = max(scores)
        elif myscore==0:
            target = 17
        else:
            target = 35
        while myscore+sum(self.current_throws) < target:
            yield True
        yield False
Christian Sievers
fuente
2

Rebelde

Este bot combina la estrategia simple de Hesitate con la estrategia avanzada de la última ronda BotFor2X, intenta recordar quién es y se vuelve loco cuando descubre que vive en una ilusión.

class Rebel(Bot):

    p = []

    def __init__(self,*args):
        super().__init__(*args)
        self.hide_from_harkonnen=self.make_throw
        if self.p:
            return
        l = [0]*5+[1]
        for i in range(300):
            l.append(sum(l[i:])/6)
        m=[i/6 for i in range(1,5)]
        self.p.extend((1-sum([a*b for a,b in zip(m,l[i:])])
                                          for i in range(300) ))

    def update_state(self,*args):
        super().update_state(*args)
        self.current_sum = sum(self.current_throws)
        # remember who we are:
        self.make_throw=self.hide_from_harkonnen

    def expect(self,mts,ops):
        p = 1
        for s in ops:
            p *= self.p[mts-s]
        return p

    def throw_again(self,mts,ops):
        ps = self.expect(mts,ops)
        pr = sum((self.expect(mts+d,ops) for d in range(1,6)))/6
        return pr>ps

    def make_throw(self, scores, last_round):
        myscore = scores[self.index]
        if len(self.current_throws)>1:
            # hello Tleilaxu!
            target = 666
        elif last_round:
            target = max(scores)
        elif myscore==0:
            target = 17
        else:
            target = 35
        while myscore+self.current_sum < target:
            yield True
        if myscore+self.current_sum < 40:
            yield False
        opscores = scores[self.index+1:] + scores[:self.index]
        for i in range(len(opscores)):
            if opscores[i]>=40:
                opscores = opscores[:i]
                break
        while True:
            yield self.throw_again(myscore+self.current_sum,opscores)
Christian Sievers
fuente
Bueno, esto es bastante elegante :) Además, ¡felicidades por obtener el primer y segundo lugar en la competencia principal!
Dani O
Naturalmente, lo modifiqué HarkonnenBotpara que Rebelya no se pueda deshacer;) y también lo modifiqué TleilaxuBotpara que Rebelya no lo detecte.
Dani O
1

Toma cinco

class TakeFive(Bot):
    def make_throw(self, scores, last_round):
        # Throw until we hit a 5.
        while self.current_throws[-1] != 5:
            # Don't get greedy.
            if scores[self.index] + sum(self.current_throws) >= self.end_score:
                break
            yield True

        # Go for the win on the last round.
        if last_round:
            while scores[self.index] + sum(self.current_throws) <= max(scores):
                yield True

        yield False

La mitad del tiempo, sacaremos un 5 antes que un 6. Cuando lo hagamos, retiraremos.

Mnemotécnico
fuente
Si nos detenemos en 1 en su lugar, avanza más lentamente, pero es más probable que llegue a 40 de un solo salto.
Mnemónico
En mis pruebas, TakeOne obtuvo 20.868 puntos por ronda en comparación con los 24.262 puntos de TakeFive por ronda (y también aumentó la tasa de ganancia de 0.291 a 0.259). Así que no creo que valga la pena.
Spitemaster
1

Cazador

class Chaser(Bot):
    def make_throw(self, scores, last_round):
        while max(scores) > (scores[self.index] + sum(self.current_throws)):
            yield True
        while last_round and (scores[self.index] + sum(self.current_throws)) < 44:
            yield True
        while self.not_thrown_firce() and sum(self.current_throws, scores[self.index]) < 44:
            yield True
        yield False

    def not_thrown_firce(self):
        return len(self.current_throws) < 4

El cazador intenta alcanzar la posición uno. Si es la última ronda, intenta desesperadamente alcanzar al menos 50 puntos. Solo por si acaso, lanza al menos cuatro veces, pase lo que pase.

[editar 1: estrategia de ir por el oro agregada en la última ronda]

[editar 2: lógica actualizada porque pensé erróneamente que un bot obtendría una puntuación de 40 en lugar de solo la puntuación más alta del bot]

[editar 3: hizo al cazador un poco más defensivo en el juego final]

AKroell
fuente
Bienvenido a PPCG! Buena idea para no solo tratar de ponerse al día, sino también pasar el primer lugar. Estoy ejecutando una simulación en este momento, ¡y te deseo suerte!
maxb
¡Gracias! Inicialmente intenté superar al líder anterior en una cantidad fija (valores probados entre 6 y 20), pero resulta que simplemente lanzo dos veces más ferias mejor.
AKroell
@JonathanFrech gracias, arreglado
AKroell
1

FutureBot

class FutureBot(Bot):
    def make_throw(self, scores, last_round):
        while (random.randint(1,6) != 6) and (random.randint(1,6) != 6):
            current_score = scores[self.index] + sum(self.current_throws)
            if current_score > (self.end_score+5):
                break
            yield True
        yield False

OneStepAheadBot

class OneStepAheadBot(Bot):
    def make_throw(self, scores, last_round):
        while random.randint(1,6) != 6:
            current_score = scores[self.index] + sum(self.current_throws)
            if current_score > (self.end_score+5):
                break
            yield True
        yield False

Un par de bots, traen sus propios juegos de dados y los tira para predecir el futuro. Si uno es un 6, se detienen, FutureBot no puede recordar cuál de sus 2 dados fue para el próximo lanzamiento, por lo que se rinde.

Me pregunto cuál lo hará mejor.

OneStepAhead es un poco demasiado similar a OneInFive para mi gusto, pero también quiero ver cómo se compara con FutureBot y OneInFive.

Editar: ahora se detienen después de llegar a 45

william porter
fuente
Bienvenido a PPCG! ¡Tu bot definitivamente juega con el espíritu del juego! Haré una simulación más tarde esta noche.
maxb
¡Gracias! Tengo curiosidad por saber qué tan bien le irá, pero supongo que será bajo.
william porter