A game of dice, but avoid number 6

57

15

Tournament over!

The tournament is now over! The final simulation was run during the night, a total of \$3*10^8\$ games. The winner is Christian Sievers with his bot OptFor2X. Christian Sievers also managed to secure the second place with Rebel. Congratulations! Below you can see the official high score list for the tournament.

If you still want to play the game, you are more than welcome to use the controller posted below, and to use the code in it to create your own game.

Dice

I was invited to play a game of dice which I had never heard of. The rules were simple, yet I think it would be perfect for a KotH challenge.

The rules

The start of the game

The die goes around the table, and each time it is your turn, you get to throw the die as many times as you want. However, you have to throw it at least once. You keep track of the sum of all throws for your round. If you choose to stop, the score for the round is added to your total score.

So why would you ever stop throwing the die? Because if you get 6, your score for the entire round becomes zero, and the die is passed on. Thus, the initial goal is to increase your score as quickly as possible.

Who is the winner?

When the first player around the table reaches 40 points or more, the last round starts. Once the last round has started, everyone except the person who initiated the last round gets one more turn.

The rules for the last round is the same as for any other round. You choose to keep throwing or to stop. However, you know that you have no chance of winning if you don't get a higher score than those before you on the last round. But if you keep going too far, then you might get a 6.

However, there's one more rule to take into consideration. If your current total score (your previous score + your current score for the round) is 40 or more, and you hit a 6, your total score is set to 0. That means that you have to start all over. If you hit a 6 when your current total score is 40 or more, the game continues as normal, except that you're now in last place. The last round is not triggered when your total score is reset. You could still win the round, but it does become more challenging.

The winner is the player with the highest score once the last round is over. If two or more players share the same score, they will all be counted as victors.

An added rule is that the game continues for a maximum of 200 rounds. This is to prevent cases where multiple bots basically keep throwing until they hit 6 to stay at their current score. Once the 199th round is passed, last_round is set to true, and one more round is played. If the game goes to 200 rounds, the bot (or bots) with the highest score is the winner, even if they do not have 40 points or more.

Recap

  • Each round you keep throwing the die until you choose to stop or you get a 6
  • You must throw the die once (if your first throw is a 6, your round is immediately over)
  • If you get a 6, your current score is set to 0 (not your total score)
  • You add your current score to your total score after each round
  • When a bot ends their turn resulting in a total score of at least 40, everyone else gets a last turn
  • If your current total score is \$\geq 40\$ and you get a 6, your total score is set to 0 and your round is over
  • The last round is not triggered when the above occurs
  • The person with the highest total score after the last round is the winner
  • In case there are multiple winners, all will be counted as winners
  • The game lasts for a maximum of 200 rounds

Clarification of the scores

  • Total score: the score that you have saved from previous rounds
  • Current score: the score for the current round
  • Current total score: the sum of the two scores above

How do you participate

To participate in this KotH challenge, you should write a Python class which inherits from Bot. You should implement the function: make_throw(self, scores, last_round). That function will be called once it is your turn, and your first throw was not a 6. To keep throwing, you should yield True. To stop throwing, you should yield False. After each throw, the parent function update_state is called. Thus, you have access to your throws for the current round using the variable self.current_throws. You also have access to your own index using self.index. Thus, to see your own total score you would use scores[self.index]. You could also access the end_score for the game by using self.end_score, but you can safely assume that it will be 40 for this challenge.

You are allowed to create helper functions inside your class. You may also override functions existing in the Bot parent class, e.g. if you want to add more class properties. You are not allowed to modify the state of the game in any way except yielding True or False.

You're free to seek inspiration from this post, and copy any of the two bots that I've included here. However, I'm afraid that they're not particularly effective...

On allowing other languages

In both the sandbox and on The Nineteenth Byte, we have had discussions about allowing submissions in other languages. After reading about such implementations, and hearing arguments from both sides, I have decided to restrict this challenge to Python only. This is due to two factors: the time required to support multiple languages, and the randomness of this challenge requiring a high number of iterations to reach stability. I hope that you will still participate, and if you want to learn some Python for this challenge, I'll try to be available in the chat as often as possible.

For any questions that you might have, you can write in the chat room for this challenge. See you there!

Rules

  • Sabotage is allowed, and encouraged. That is, sabotage against other players
  • Any attempt to tinker with the controller, run-time or other submissions will be disqualified. All submissions should only work with the inputs and storage they are given.
  • Any bot which uses more than 500MB memory to make its decision will be disqualified (if you need that much memory you should rethink your choices)
  • A bot must not implement the exact same strategy as an existing one, intentionally or accidentally.
  • You are allowed to update your bot during the time of the challenge. However, you could also post another bot if your approach is different.

Example

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

This bot will keep going until it has a score of at least 10 for the round, or it throws a 6. Note that you don't need any logic to handle throwing 6. Also note that if your first throw is a 6, make_throw is never called, since your round is immediately over.

For those who are new to Python (and new to the yield concept), but want to give this a go, the yield keyword is similar to a return in some ways, but different in other ways. You can read about the concept here. Basically, once you yield, your function will stop, and the value you yielded will be sent back to the controller. There, the controller handles its logic until it is time for your bot to make another decision. Then the controller sends you the dice throw, and your make_throw function will continue executing right where if stopped before, basically on the line after the previous yield statement.

This way, the game controller can update the state without requiring a separate bot function call for each dice throw.

Specification

You may use any Python library available in pip. To ensure that I'll be able to get a good average, you have a 100 millisecond time limit per round. I'd be really happy if your script was way faster than that, so that I can run more rounds.

Evaluation

To find the winner, I will take all bots and run them in random groups of 8. If there are fewer than 8 classes submitted, I will run them in random groups of 4 to avoid always having all bots in each round. I will run simulations for about 8 hours, and the winner will be the bot with the highest win percentage. I will run start the final simulations at the start of 2019, giving you all Christmas to code your bots! The preliminary final date is January 4th, but if that's too little time I can change it to a later date.

Until then, I'll try to make a daily simulation using 30-60 minutes of CPU time, and updating the score board. This will not be the official score, but it will serve as a guide to see which bots perform the best. However, with Christmas coming up, I hope you can understand that I won't be available at all times. I'll do my best to run simulations and answer any questions related to the challenge.

Test it yourself

If you want to run your own simulations, here's the full code to the controller running the simulation, including two example bots.

Controller

Here's the updated controller for this challenge. It supports ANSI outputs, multi-threading, and collects additional stats thanks to AKroell! When I make changes to the controller, I'll update the post once documentation is complete.

Thanks to BMO, the controller is now able to download all bots from this post using the -d flag. Other functionality is unchanged in this version. This should ensure that all of your latest changes are simulated as soon as possible!

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

If you want access to the original controller for this challenge, it is available in the edit history. The new controller has the exact same logic for running the game, the only difference is performance, stat collection and prettier printing.

Bots

On my machine, the bots are kept in the file forty_game_bots.py. If you use any other name for the file, you must update the import statement at the top of the controller.

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

Running the simulation

To run a simulation, save both code snippets posted above to two separate files. I have saved them as forty_game_controller.py and forty_game_bots.py. Then you simply use python forty_game_controller.py or python3 forty_game_controller.py depending on your Python configuration. Follow the instructions from there if you want to configure your simulation further, or try tinkering with the code if you want.

Game stats

If you're making a bot that aims for a certain score without taking other bots into consideration, these are the winning score percentiles:

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

High scores

As more answers are posted, I'll try to keep this list updated. The contents of the list will always be from the latest simulation. The bots ThrowTwiceBot and GoToTenBot are the bots from the code above, and are used as reference. I did a simulation with 10^8 games, which took about 1 hour. Then I saw that the game reached stability compared to my runs with 10^7 games. However, with people still posting bots, I won't do any longer simulations until the frequency of responses has gone down.

I try to add all new bots and add any changes that you've made to existing bots. If it seems that I have missed your bot or any new changes you have, write in the chat and I'll make sure to have your very latest version in the next simulation.

We now have more stats for each bot thanks to AKroell! The three new columns contain the maximum score across all games, the average score per game, and the average score when winning for each bot.

As pointed out in the comments, there was an issue with the game logic which made bots that had a higher index within a game get an extra round in some cases. This has been fixed now, and the scores below reflect this.

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|
+-----------------------+----+--------+--------+------+------+-------+------+--------+

The following bots (except Rebel) are made to bend the rules, and the creators have agreed to not take part in the official tournament. However, I still think their ideas are creative, and they deserve a honorable mention. Rebel is also on this list because it uses a clever strategy to avoid sabotage, and actually performs better with the sabotaging bot in play.

The bots NeoBot and KwisatzHaderach does follow the rules, but uses a loophole by predicting the random generator. Since these bots take a lot of resources to simulate, I have added its stats from a simulation with fewer games. The bot HarkonnenBot achieves victory by disabling all other bots, which is strictly against the rules.

    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

Posted 2018-12-19T08:16:03.627

Reputation: 5 754

Question was closed 2019-09-04T19:59:35.743

2So maybe the rules would be slightly clearer if they said "when a player ends their turn with a score of at least 40, everyone else gets a last turn". This avoids the apparent conflict by pointing out it's not reaching 40 that really triggers the last round, it's stopping with at least 40. – aschepler – 2018-12-19T22:15:49.107

For the get_all_bots function, why not just use Bot.__subclasses__()? – NoOneIsHere – 2018-12-20T04:43:44.117

@NoOneIsHere that's probably a better way to do it. I would say that I'm well versed in Python, but it's not my main language. But now I can change it, thanks for the tip! – maxb – 2018-12-20T05:07:58.360

1@aschepler that's a good formulation, I'll edit the post when I'm on my computer – maxb – 2018-12-20T05:13:30.203

2

@maxb I've extended the controller to add more stats that were relevant to my development process: highest score reached, average score reached and average winning score https://gist.github.com/A-w-K/91446718a46f3e001c19533298b5756c

– AKroell – 2018-12-20T12:49:41.000

1@AKroell Thanks for the addition! I have also made some ongoing changes to get more stats, but mostly related to bot runtimes and checking for ties. I'll try to look through your additions later today and update it. – maxb – 2018-12-20T12:58:03.560

2

This sounds very similar to a very fun dice game called Farkled https://en.wikipedia.org/wiki/Farkle

– Caleb Jay – 2018-12-20T19:02:20.397

How is the rolling priority decided? So it seems like whenever someone ends their turn with 40+ the last round is triggered. However, if the 5th person ends with 40+, the 6,7,8th rollers get one less round than 1,2,3,4 th players, which i feel is an unfair advantage. I mean randomness should go away with high number of simulations but still:)) – Ofya – 2018-12-21T16:46:55.967

It seems to me that in the controller the line if game_score[index] >= self.end_score: should include and not last_round, otherwise you may reassign the last_round_initiator – Christian Sievers – 2018-12-21T17:30:00.067

@Ofya thank you for pointing that out. That's actually something that should be changed. I'll look at it when I get home. – maxb – 2018-12-21T18:11:54.567

@maxb also in that case we need to think about what happens if 2 or more people get above 40 on the same round – Ofya – 2018-12-21T18:38:31.603

@Ofya according to the rules, as soon as one player reaches 40 points, all other players get one more turn, regardless of who started the game. The code does not reflect this, and should be changed. It should not affect the highscore list, but it should improve stability – maxb – 2018-12-21T18:48:12.840

I'm interested in a comparison between goToTenBot and a goToElevenBot. I think 11 would be better. – Mooing Duck – 2018-12-21T21:16:55.250

1@MooingDuck I have done my own comparisons and calculations before posting this challenge, and 11 is definitely better than 10. The maximum is around 16 if you're aiming for average score per round. The example bots I added were not the greatest on purpose, I wanted to leave as much exploration as possible to everyone else. – maxb – 2018-12-21T23:26:55.417

A theoretical concern: imagine you're the last player in the round, it is last_round, the maximum score is 40, reached by player 2, your current total score is 41, and player 1 has score 39. You may want to throw again, because player 1 will throw after you and has a good chance of beating 41. Or will he? It could be the 200th round, in which case you need not take any risk, you can just stop now and win. So it's important to know if player 1 will get to throw again, you would know it in a real game played by humans, but there is no documented way to get this information. – Christian Sievers – 2019-01-02T22:40:26.007

@ChristianSievers that is true, it would be beneficial to see who the initiator of the last round is. However, I still haven't seen a game go beyond 30 rounds. In the 300 million games ran for the last simulation, the highest round reached was 23. I doubt that this will change significantly within the scope of the tournament, so you can safely assume that it is never round 200. I thought of sending the round number to each bot, but didn't implement that for similar reasons. – maxb – 2019-01-02T23:39:07.010

1Have you considered running a simulation of games with a max of 10 rounds? It seems like it'd change the rankings up quite a bit. I'm curious as too how it'd affect things. – william porter – 2019-01-02T23:52:00.213

@maxb I agree, that's why I called it a theoretical concern. Maybe something to keep in mind for the next challenge. One could also argue that it'd be nice to see the other bots' behaviour. It might be interesting to know if a bot with score 0 got a 6 immediately or didn't stop with current score 30. – Christian Sievers – 2019-01-03T00:32:26.753

5I'm voting to close this question because it's already de-facto closed to new answers ("The tournament is now over! The final simulation was run during the night, a total of 3∗108 games") – pppery – 2019-09-04T11:43:07.840

1@pppery The tournament is definitely over, it was a long time since the last answer. However, I couldn't find that KotH challenges are usually closed after a certain amount of time? – maxb – 2019-09-04T11:59:58.507

1

@maxb See the consensus here

– caird coinheringaahing – 2019-09-04T12:07:33.453

Answers

7

OptFor2X

This bot follows an approximation to the optimal strategy for the two player version of this game, using only its score and the score of the best opponent. In the last round, the updated version considers all scores.

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

Posted 2018-12-19T08:16:03.627

Reputation: 6 366

I'll look the implementation through as soon as I can. With Christmas celebrations, it might not be until the 25th – maxb – 2018-12-23T23:24:33.083

Your bot is in the lead! Also, There is no need to make it run faster, it is approximately as quick as all other bots at making decisions. – maxb – 2018-12-26T12:08:08.640

I didn't want to make it faster. I already did what I wanted to do - initialize only once -, but was looking for a nicer way to do it, especially without defining functions outside the class. I think it is better now. – Christian Sievers – 2018-12-27T22:45:53.987

It looks a lot better now, good work! – maxb – 2018-12-27T23:09:58.890

Congratulations on securing both first and second place! – maxb – 2019-01-06T14:55:52.193

21

NeoBot

Instead, only try to realize the truth - there is no spoon

NeoBot peeks into the matrix (aka random) and predicts if the next roll will be a 6 or not - it can't do anything about being handed a 6 to start with but is more than happy to dodge a streak ender.

NeoBot doesn't actually modify the controller or runtime, just politely asks the library for more information.

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

Mostly Harmless

Posted 2018-12-19T08:16:03.627

Reputation: 359

1Welcome to PPCG! This is a really impressive answer. When I first ran it, I was bothered by the fact that it used the same amount of runtime as all other bots combined. Then I looked at the win percentage. Really clever way of skirting the rules. I will allow your bot to participate in the tournament, but I hope that others refrain from using the same tactic as this, as it violates the spirit of the game. – maxb – 2018-12-24T09:25:03.933

2Since there is such a huge gap between this bot and the second place, combined with the fact that your bot requires a lot of computing, would you accept that I run a simulation with fewer iterations to find your win rate, and then run the official simulation without your bot? – maxb – 2018-12-24T09:28:14.033

3Fine by me, I figured going in that this was likely disqualifiable and definitely not quite in the spirit of the game. That being said, it was a blast to get working and a fun excuse to poke around in the python source code. – Mostly Harmless – 2018-12-24T14:14:35.833

2thanks! I don't think any other bot will get close to your score. And for anyone else thinking about implementing this strategy, don't. From now on this strategy is against the rules, and NeoBot is the only one allowed to use it for the sake of keeping the tournament fair. – maxb – 2018-12-24T15:13:44.987

While it is certainly not modifying, I'm not so sure if it isn't tinkering. I guess I better leave this to native speakers. - This bot also seems very sensitive to changes of unspecified implementation details of the controller... – Christian Sievers – 2018-12-31T16:37:29.383

1Well, myBot beats every one, but this is way too much better - i though if I would post bot like this, i would get -100 and not best score. – Jan Ivan – 2019-01-02T10:55:41.777

@ChristianSievers Yup, it's fragile - as written, it assumes that the next call to random will be the dice roll, and that random is being initialized in the context of the calling function. You could certainly modify the controller in various ways to make the current version fail, but unless you switch away from using python's built-in pseudorandom class or create a version of the controller for the official scoring not shared publicly, there's no reason the bot couldn't be updated. Honestly, this is less a serious entry and more "building your code this way has some funny implications..." – Mostly Harmless – 2019-01-03T03:35:52.863

15

Cooperative Swarm

Strategy

I don't think anyone else has yet noticed the significance of this rule:

If the game goes to 200 rounds, the bot (or bots) with the highest score is the winner, even if they do not have 40 points or more.

If every bot always rolled until they busted, then everyone would have a score of zero at the end of round 200 and everybody would win! Thus, the Cooperative Swarm's strategy is to cooperate as long as all players have a score of zero, but to play normally if anybody scores any points.

In this post, I am submitting two bots: the first is CooperativeSwarmBot, and the second is CooperativeThrowTwice. CooperativeSwarmBot serves as a base class for all bots that are formally part of the cooperative swarm, and has placeholder behavior of simply accepting its first successful roll when cooperation fails. CooperativeSwarmBot has CooperativeSwarmBot as its parent and is identical to it in every way except that its non-cooperative behavior is to make two rolls instead of one. In the next few days I will be revising this post to add new bots that use much more intelligent behavior playing against non-cooperative bots.

Code

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

Analysis

Viability

It is very hard to cooperate in this game because we need the support of all eight players for it to work. Since each bot class is limited to one instance per game, this is a hard goal to achieve. For example, the odds of choosing eight cooperative bots from a pool of 100 cooperative bots and 30 non-cooperative bots is:

$$\frac{100}{130} * \frac{99}{129} * \frac{98}{128} * \frac{97}{127} * \frac{96}{126} * \frac{95}{125} * \frac{94}{124} * \frac{93}{123} \approx 0.115$$

More generally, the odds of choosing \$i\$ cooperative bots from a pool of \$c\$ cooperative bots and \$n\$ noncooperative bots is:

$$\frac{c! \div (c - i)!}{(c+n)! \div (c + n - i)!}$$

From this equation we can easily show that we would need about 430 cooperative bots in order for 50% of games to end cooperatively, or about 2900 bots for 90% (using \$i = 8\$ as per the rules, and \$n = 38\$).

Case Study

For a number of reasons (see footnotes 1 and 2), a proper cooperative swarm will never compete in the official games. As such, I'll be summarizing the results of one of my own simulations in this section.

This simulation ran 10000 games using the 38 other bots that had been posted here the last time I checked and 2900 bots that had CooperativeSwarmBot as their parent class. The controller reported that 9051 of the 10000 games (90.51%) ended at 200 rounds, which is quite close to the prediction that 90% of games would be cooperative. The implementation of these bots was trivial; other than CooperativeSwarmBot they all took this form:

class CooperativeSwarm_1234(CooperativeSwarmBot):
    pass

Less that 3% of the bots had a win percentage that was below 80%, and just over 11% of the bots won every single game they played. The median win percentage of the 2900 bots in the swarm is about 86%, which is outrageously good. For comparison, the top performers on the current official leaderboard win less than 22% of their games. I can't fit the full listing of the cooperative swarm within the maximum allowed length for an answer, so if you want to view that you'll have to go here instead: https://pastebin.com/3Zc8m1Ex

Since each bot played in an average of about 27 games, luck plays a relatively large roll when you look at the results for individual bots. As I have not yet implemented an advanced strategy for non-cooperative games, most other bots benefited drastically from playing against the cooperative swarm, performing even the cooperative swarm's median win rate of 86%.

The full results for bots that aren't in the swarm are listed below; there are two bots whose results I think deserve particular attention. First, StopBot failed to win any games at all. This is particularly tragic because the cooperative swarm was actually using the exact same strategy as StopBot was; you would have expected StopBot to win an eight of its games by chance, and a little bit more because the cooperative swarm is forced to give its opponents the first move. The second interesting result, however, is that PointsAreForNerdsBot's hard work finally paid off: it cooperated with the swarm and managed to win every single game it played!

+---------------------+----+--------+--------+------+------+-------+------+--------+
|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|
+---------------------+----+--------+--------+------+------+-------+------+--------+

Flaws

There are a couple of drawbacks to this cooperative approach. First, when playing against non-cooperative bots cooperative bots never get the first-turn advantage because when they do play first, they don't yet know whether or not their opponents are willing to cooperate, and thus have no choice but to get a score of zero. Similarly, this cooperative strategy is extremely vulnerable to exploitation by malicious bots; for instance, during cooperative play the bot who plays last in the last round can choose to stop rolling immediately to make everybody else lose (assuming, of course, that their first roll wasn't a six).

By cooperating, all bots can achieve the optimal solution of a 100% win rate. As such, if the win rate was the only thing that mattered then cooperation would be a stable equilibrium and there would be nothing to worry about. However, some bots might prioritize other goals, such as reaching the top of the leaderboard. This means that there is a risk that another bot might defect after your last turn, which creates an incentive for you to defect first. Because the setup of this competition doesn't allow us to see what our opponents did in their prior games, we can't penalize individuals that defected. Thus, cooperation is ultimately an unstable equilibrium doomed for failure.

Footnotes

[1]: The primary reasons why I don't want to submit thousands of bots instead of just two are that doing so would slow the simulation by a factor on the order of 1000 [2], and that doing so would significantly mess with win percentages as other bots would almost exclusively be playing against the swarm rather than each other. More important, however, is the fact that even if I wanted to I wouldn't be able to make that many bots in a reasonable time frame without breaking the spirit of the rule that "A bot must not implement the exact same strategy as an existing one, intentionally or accidentally".

[2]: I think there are two main reasons that the simulation slows down when running a cooperative swarm. First, more bots means more games if you want each bot to play in the same number of games (in the case study, the number of games would differ by a factor of about 77). Second, cooperative games just take longer because they last for a full 200 rounds, and within a round players have to keep rolling indefinitely. For my setup, games took about 40 times longer to simulate: the case study took a little over three minutes to run 10000 games, but after removing the cooperative swarm it would finish 10000 games in just 4.5 seconds. Between these two reasons, I estimate it would take about 3100 times longer to accurately measure the performance of bots when there is a swarm competing compared to when there isn't.

Einhaender

Posted 2018-12-19T08:16:03.627

Reputation: 151

4Wow. And welcome to PPCG. This is quite the first answer. I wasn't really planning on a situation like this. You certainly found a loophole in the rules. I'm not really sure how I should score this, since your answer is a collection of bots rather than a single bot. However, the only thing I'll say right now is that it feels unfair that one participant would control 98.7% of all bots. – maxb – 2018-12-22T16:37:18.570

2I actually don't want duplicate bots to be in the official competition; that's why I ran the simulation myself instead of submitting thousands of very nearly identical bots. I'll revise my submission to make that more clear. – Einhaender – 2018-12-22T17:09:28.110

1Had I anticipated an answer like this, I would have changed the games that go to 200 rounds so that they don't give scores to players. However, as you note, there is a rule about creating identical bots which would make this strategy be against the rules. I'm not going to change the rules, as it would be unfair to everyone who has made a bot. However, the concept of cooperation is very interesting, And I hope that there are other bots submitted which implement the cooperation strategy in combination with its own unique strategy. – maxb – 2018-12-22T17:10:12.167

1I think your post is clear after reading it more thoroughly. – maxb – 2018-12-22T17:11:27.380

How many existing bots would need to wrap their code in this cooperation framework in order for a majority of them to see a net gain in their leaderboard placement? My naive guess is 50%. – Sparr – 2019-01-04T00:08:05.250

10

Adaptive Roller

Starts out more aggressive and calms down towards the end of the round.
If it believes it's winning, roll an extra time for safety.

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

Posted 2018-12-19T08:16:03.627

Reputation: 50 798

Great first submission! I'll run it against my bots I wrote for testing, but I'll update the highscore when more bots have been posted. – maxb – 2018-12-19T08:51:22.973

I ran some tests with slight modifications to your bot. lim = max(min(self.end_score - scores[self.index], 24), 6) raising the maximum to 24 and adding a minimum of 6 both increase the winning percentage on their own and even more so combined. – AKroell – 2018-12-20T16:25:05.837

@AKroell: Cool! I have intended to do something similar to make sure that it rolls a few times at the end, but I haven't taken myself the time to do it yet. Weirdly though, it seems to perform worse with those values when I do 100k runs. I've only tested with 18 bots though. Maybe I should do some tests with all bots. – Emigna – 2018-12-20T17:50:50.760

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

Just have a try with all GoToNBot's, And 20, 22, 24 plays best. I don't know why.


Update: always stop throw if get score 40 or more.

tsh

Posted 2018-12-19T08:16:03.627

Reputation: 13 072

I have also experimented with those kinds of bots. The highest average score per round is found when the bot goes to 16, but I'm assuming that the "end game" makes the 20-bot win more often. – maxb – 2018-12-19T08:57:48.160

@maxb Not so, 20 still be the best one without the "end game" in my test. Maybe you had tested it on the old version of controller. – tsh – 2018-12-19T09:00:52.620

I ran a separate test before designing this challenge, where I calculated the average score per round for the two tactics in my post ("throw x times" and "throw until x score"), and the maximum I found was for 15-16. Though my sample size could have been too small, I did notice instability. – maxb – 2018-12-19T09:04:21.497

@maxb I meat the old controller which not break when yield False, make all bots throw another time. And 16 became the best one instead of 20 due to this reason. – tsh – 2018-12-19T09:06:04.390

15 should be the break-even point past which your expected score after the next roll goes down instead of up. (Possible scores are 16, 17, 18, 19, 20 and 0, whose average is 15.) – Neil – 2018-12-19T09:49:41.863

@Neil Yes, That's in theory for getting highest scores in one turn. But highest score in one turn do not mean highest score when game finish. – tsh – 2018-12-19T09:54:34.107

2I have done some testing with this, and my conclusion is simply that 20 works well because it is 40/2. Though I'm not completely sure. When I set end_score to 4000 (and changed your bot to use this in the target calculation), the 15-16 bots were quite a lot better. But if the game was only about increasing your score it would be trivial. – maxb – 2018-12-19T11:23:08.847

1@maxb If end_score is 4000, it is almost impossible to get 4000 before 200 turns. And the game is simply who got the highest score in 200 turns. And stop at 15 should works will since this time the strategy for highest score in one turn is as same as highest score in 200 turns. – tsh – 2018-12-21T01:39:19.597

@tsh That's correct. I failed to increase the max_rounds for my simulation. When I did it now, I saw that all games timed out at 200 rounds. GoTo20Bot is fairly insensitive to changes in end_score, at least compared to GoHomeBot... – maxb – 2018-12-21T08:23:11.873

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

The idea is that other bots may lose points, so being 2nd isn't bad - but if you're very behind, you might as well go for broke.

Stuart Moore

Posted 2018-12-19T08:16:03.627

Reputation: 181

1Welcome to PPCG! I'm looking through your submission, and it seems that the more players are in the game, the lower the win percentage is for your bot. I can't tell why straight away. With bots being matched 1vs1 you get a 10% winrate. The idea sounds promising, and the code looks correct, so I can't really tell why your winrate isn't higher. – maxb – 2018-12-19T13:31:37.877

6I have looked into the behavior, and this line had me confused: 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. Even though your bot is in the lead after 7 throws, it continues until it hits a 6. As I'm typing this I figured out the issue! The scores only contain the total scores, not the die cases for the current round. You should modify it to be current_score = scores[self.index] + sum(self.current_throws). – maxb – 2018-12-19T13:49:27.770

Thanks - will make that change! – Stuart Moore – 2018-12-19T13:52:37.523

5

Alpha

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 refuses ever to be second to anyone. So long as there is a bot with a higher score, it will keep rolling.

user48543

Posted 2018-12-19T08:16:03.627

Reputation:

Because of how yield works, if it starts rolling it will never stop. You'll want to update my_score in the loop. – Spitemaster – 2018-12-19T17:14:46.977

@Spitemaster Fixed, thanks. – None – 2018-12-19T17:24:20.407

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

We want to go big or go home, right? GoHomeBot mostly just goes home. (But does surprisingly well!)

Spitemaster

Posted 2018-12-19T08:16:03.627

Reputation: 695

Since this bot always goes for 40 points, it will never have any points in the scores list. There was a bot like this before (the GoToEnd bot), but david deleted their answer. I'll replace that bot by yours. – maxb – 2018-12-20T05:57:23.283

1It's quite funny, seeing this bots' expaned stats: Except for pointsAreForNerds and StopBot, this bot has the lowest average points, and yet it has a nice win ratio – Belhenix – 2018-12-20T19:48:19.213

5

EnsureLead

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

EnsureLead borrows ideas from GoTo20Bot. It adds the concept that it always considers (when in last_round or reaching 40) that there are others which will have at least one more roll. Thus, the bot tries to get a bit ahead of them, such that they have to catch up.

Dirk Herrmann

Posted 2018-12-19T08:16:03.627

Reputation: 151

4

Roll6TimesV2

Doesn't beat the current best, but I think it will fair better with more bots in play.

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

Really awesome game by the way.

itsmephil12345

Posted 2018-12-19T08:16:03.627

Reputation: 41

Welcome to PPCG! Very impressive for not only your first KotH challenge, but your first answer. Glad that you liked the game! I have had lots of discussion about the best tactic for the game after the evening when I played it, so it seemed perfect for a challenge. You're currently in third place out of 18. – maxb – 2018-12-19T20:05:31.630

4

StopBot

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

Literally only one throw.

This is equivalent to the base Bot class.

Zacharý

Posted 2018-12-19T08:16:03.627

Reputation: 5 710

1Don't be sorry! You're following all the rules, though I'm afraid that your bot is not terribly effective with an average of 2.5 points per round. – maxb – 2018-12-19T19:42:57.040

1I know, somebody had to post that bot though. Degenerate bots for the loss. – Zacharý – 2018-12-19T21:04:43.657

5I'd say that I'm impressed by your bot securing exactly one win in the last simulation, proving that it's not completely useless. – maxb – 2018-12-20T08:28:49.900

2IT WON A GAME?! That is surprising. – Zacharý – 2018-12-20T21:17:48.027

3

BringMyOwn_dice (BMO_d)

This bot loves dice, it brings 2 (seems to perform the best) dice of its own. Before throwing dice in a round, it throws its own 2 dice and computes their sum, this is the number of throws it is going to perform, it only throws if it doesn't already have 40 points.

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

ბიმო

Posted 2018-12-19T08:16:03.627

Reputation: 15 345

2I was thinking of a random bot using a coin flip, but this is more in spirit with the challenge! I think that two dice perform the best, since you get the most points per round when you cast the die 5-6 times, close to the average score when casting two dice. – maxb – 2018-12-19T15:08:26.857

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

Posted 2018-12-19T08:16:03.627

Reputation: 41 901

# Must throw at least once is unneeded - it throws once before calling your bot. Your bot will always throw a minimum of twice. – Spitemaster – 2018-12-19T15:50:41.133

Thanks. I was misled by the name of the method. – Peter Taylor – 2018-12-19T16:00:55.240

@PeterTaylor Thanks for your submission! I named the make_throw method early on, when I wanted players to be able to skip their turn. I guess a more appropriate name would be keep_throwing. Thanks for the feedback in the sandbox, it really helped make this a proper challenge! – maxb – 2018-12-20T09:02:53.863

3

Go Big Early

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

Concept: Try to win big on an early roll (getting to 25) then creep up from there 2 rolls at a time.

Stuart Moore

Posted 2018-12-19T08:16:03.627

Reputation: 181

3

BinaryBot

Tries to get close to the end score, so that as soon as somebody else triggers the last round it can beat their score for the win. Target is always halfway between current score and end score.

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

Cain

Posted 2018-12-19T08:16:03.627

Reputation: 1 149

Interesting, Hesitate also refuses to cross the line first. You need to surround your function with the class stuff. – Christian Sievers – 2018-12-19T22:15:00.143

3

PointsAreForNerdsBot

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

This one needs no explanation.

OneInFiveBot

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

Keeps rolling until it rolls a five on it's own 5-sided die. Five is less than six, so it HAS TO WIN!

The_Bob

Posted 2018-12-19T08:16:03.627

Reputation: 47

2Welcome to PPCG! I'm sure you're aware, but your first bot is literally the worst bot in this competition! The OneInFiveBot is a neat idea, but I think it suffers in the end game compared to some of the more advanced bots. Still a great submission! – maxb – 2018-12-20T08:34:28.757

2the OneInFiveBot is quite interesting in the way that he consistently has the highest overall score reached. – AKroell – 2018-12-20T14:37:29.937

1Thanks for giving StopBot a punching bag :P. The OneInFiveBot actually is pretty neat, nice job! – Zacharý – 2018-12-20T21:25:01.843

@maxb Yep, that's where I got the name. I honestly didn't test OneInFiveBot and it's doing much better than I expected – The_Bob – 2018-12-21T05:55:44.460

4

I'm afraid that to stick to community norms, PointsAreForNerdsBots should be deleted.

– Peter Taylor – 2018-12-21T21:54:44.963

3

SlowStart

This bot implements the TCP Slow Start algorithm. It adjusts its number of rolls (nor) according to its previous turn: if it didn't roll a 6 in the previous turn, increases the nor for this turn; whereas it reduces nor if it did.

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

quite.SimpLe

Posted 2018-12-19T08:16:03.627

Reputation: 31

Welcome to PPCG! Interesting approach, I don't know how sensitive it is to random fluctuations. Two things that are needed to make this run: def updateValues(): should be def updateValues(self): (or def update_values(self): if you want to follow PEP8). Secondly, the call updateValues() should instead be self.updateValues() (or self.update_vales()). – maxb – 2018-12-20T11:27:41.027

2Also, I think you need to update your i variable in the while loop. Right now your bot either passes the while loop entirely or is stuck in the while loop until it hits 6. – maxb – 2018-12-20T11:32:58.713

In the current highscore, I took the liberty of implementing these changes. I think you could experiment with the initial value for self.nor and see how it affects the performance of your bot. – maxb – 2018-12-20T12:25:16.543

3

LizduadacBot

Tries to win in 1 step. End condition is somewhat arbritrary.

This is also my first post (and I'm new to Python), so if I beat "PointsAreForNerdsBot", I'd be happy!

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

Posted 2018-12-19T08:16:03.627

Reputation: 31

Welcome to PPCG (and welcome to Python)! You'd have a hard time losing against PointsAreForNerdsBot, but your bot actually fares quite well. I'll update the score either later tonight or tomorrow, but your winrate is about 15%, which is higher than the average 12.5%. – maxb – 2018-12-21T23:30:05.297

By "hard time", they mean it's impossible (unless I misunderstood greatly) – Zacharý – 2018-12-22T01:37:11.923

@maxb I actually didn't think the win rate would be that high! (I didn't test it out locally). I wonder if changing the 50 to be a bit higher/lower would increase the win rate. – lizduadac – 2018-12-22T23:39:44.763

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                                               

Prescience usually wins -- but destiny cannot always be avoided.
Great and mysterious are the ways of Shai-Hulud!


Back in the early days of this challenge (i.e. before NeoBot was posted), I wrote an almost-trivial Oracle bot:

    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

but didn't post it as I didn't think it was interesting enough ;) But once NeoBot went into the lead I started to think about how to beat its perfect ability to predict the future. So here's a Dune quote; it's when Paul Atreides, the Kwisatz Haderach, stands at a nexus from which an infinity of different futures can unroll:

The prescience, he realized, was an illumination that incorporated the limits of what it revealed- at once a source of accuracy and meaningful error. A kind of Heisenberg indeterminacy intervened: the expenditure of energy that revealed what he saw, changed what he saw… … the most minute action- the wink of an eye, a careless word, a misplaced grain of sand- moved a gigantic lever across the known universe. He saw violence with the outcome subject to so many variables that his slightest movement created vast shiftings in the patterns.

The vision made him want to freeze into immobility, but this, too was action with its consequences.

So here was the answer: to foresee the future is to change it; and if you're very careful, then by selective action or inaction, you can change it in an advantageous way -- at least most of the time. Even the KwisatzHaderach can't get a 100% win rate!

Dani O

Posted 2018-12-19T08:16:03.627

Reputation: 69

It seems that this bot changes the state of the random number generator, to ensure that it avoids rolling 6, or at least anticipates it. Same goes for HarkonnenBot. However, I note that the win rate of these bots are much higher than that of NeoBot. Are you actively manipulating the random number generator in order to prevent it from rolling 6? – maxb – 2019-01-02T13:13:34.390

Oh, on my first reading I didn't notice that this is not only nicer than NeoBot but also better! I also like how you give an example of what everything using randomness (especially the controller) here should do: use your own random.Random instance. Like NeoBot, this seems a bit sensitive to changes of unspecified implementation details of the controller. – Christian Sievers – 2019-01-02T22:14:23.117

@maxb: HarkonnenBot doesn't touch the RNG; it doesn't care at all about random numbers. It just poisons all the other bots, then strolls up to the finish line as slowly as possible. Like many culinary delicacies, revenge is a dish best savoured slowly, after long and delicate preparation. – Dani O – 2019-01-03T20:34:09.407

@ChristianSievers: unlike NeoBot (and HarkonnenBot), KwisatzHaderach relies on only one detail of the implementation; in particular it doesn't need to know how random.random() is implemented, only that the controller uses it ;D – Dani O – 2019-01-03T20:44:52.260

1I have looked through all of your bots. I have decided to treat KwisatzHaderach and HarkonnenBot the same way as NeoBot. They will receive their scores from a simulation with fewer games, and will not be in the official simulation. However, they will end up on the highscore list much like NeoBot. The main reason for them not being in the official simulation is that they will mess up other bot strategies. However. WisdomOfCrowds should be well suited for participation, and I'm curious about the new changes that you've made to that! – maxb – 2019-01-03T20:49:59.097

Something finally clicked, now I see exactly how this bot works. It's quite neat! As soon as I saw NeoBot, I realized that I should have used a cryptographically secure RNG. However, once that had been posted it was too late to change it. I just see it as a learning experience that will make the next KotH challenge better. – maxb – 2019-01-03T20:56:13.250

@maxb: I just updated the main post to explain the origin of the technique. I would have put it in a comment, but comments can't contain formatted code of even newlines, without which it's difficult to post readable python! – Dani O – 2019-01-03T21:08:09.223

@maxb: none of my bots are really fair competition; they all try to do something other than solve the logic of the dice game. TleilaxuBot (formerly WisdomOfCrowds) copies someone else's strategy (often NeoBot's!), KwisatzHaderach can out-Neo NeoBot by being very carefully selective about rolling the (shared) die, and HarkonnenBot just stomps everybody else into the sand ;D – Dani O – 2019-01-03T21:24:04.620

facedesk Why didn't I think of keeping my own instance of random and then copying the state over, would make things so much cleaner! (And avoid the state list rollover bug I never got around to ironing out.) I did consider adding some targeting logic, but with NeoBot being the first and clobbering everything at the time I figured it could stand as a "dumb" example of the approach. Very nicely done! – Mostly Harmless – 2019-01-04T05:11:19.467

Actually, if sum(self.current_throws) > target + random.randint(1, 6): is the really interesting bit - you already counted to the point where you know it's a 6, the call to random eats the bad roll so you can keep bumping up your score if you think you need it. Clever... – Mostly Harmless – 2019-01-04T06:44:41.553

@MostlyHarmless: thank you :) I thought of obscuring the switch between local and global RNGs a bit more (as in Oracle), but this isn't an obfuscated code challenge, so the 'roll a 6' is there in plain sight ;D – Dani O – 2019-01-04T11:48:31.857

@maxb I don't see how a cryptographically secure RNG helps when bots can reach into the internal state of the RNG. I'd still argue that you never made any guarantees about details of the controller and can use your own Random instance or just 7-randint(1,6). Next time disallow it or make it impossible by using a different language or letting the bots be separate programs. – Christian Sievers – 2019-01-04T18:04:26.460

@ChristianSievers: I think the controller should have its own private instance, whether "secure" or not; and there would be ways (either by rules, or programmatically) to prevent bots accessing the controller's private instance. And I'd agree that maxb gave no guarantees about the controller's RNG. So if it were just changed to use (an instance of) random.SystemRandom rather than (the global) random.Random then the future results would be unpredictable by any of the bots (AFAWK!) even if they could access its internal state. – Dani O – 2019-01-05T19:35:09.947

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 acts like it's always the last round and it's the last bot: it keeps rolling until it's in the lead. It also doesn't want to settle for less than 15 points unless it actually is the last round or it reaches 40 points.

Spitemaster

Posted 2018-12-19T08:16:03.627

Reputation: 695

Interesting approach. I think your bot suffers if it starts falling behind. Since the odds of getting >30 points in a single round are low, your bot is more likely to stay at its current score. – maxb – 2018-12-19T12:44:11.273

1I suspect this suffers from the same mistake I made (see NotTooFarBehindBot comments) - as in the last round, if you're not winning you'll keep throwing until you get a 6 (scores[self.index] never updates)

Actually - do you have that inequality the wrong way? max(scores) will always be >= scores[self.index] – Stuart Moore – 2018-12-19T14:09:09.157

@StuartMoore Haha, yes, I think you're right. Thanks! – Spitemaster – 2018-12-19T15:07:23.137

I suspect you want "and last_round" on the 2nd while to do what you want - otherwise the 2nd while will be used whether or not last_round is true – Stuart Moore – 2018-12-19T15:15:08.867

3That's intentional. It always tries to be in the lead when ending its turn. – Spitemaster – 2018-12-19T15:48:08.853

2

class ThrowThriceBot(Bot):

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

Well, that one is obvious

michi7x7

Posted 2018-12-19T08:16:03.627

Reputation: 405

I have done some experiments with that class of bots (it was the tactic I used when I played the game for the first time). I went with 4 throws then, though 5-6 have a higher average score per round. – maxb – 2018-12-19T13:21:43.040

Also, congratulations on your first KotH answer! – maxb – 2018-12-19T13:34:38.003

2

Hesitate

Does two modest steps, then waits for someone else to cross the line. Updated version no longer tries to beat the highscore, only wants to reach it - improving the performance by deleting two bytes of the source code!

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

Posted 2018-12-19T08:16:03.627

Reputation: 6 366

2

QuotaBot

A naive "quota" system I implemeneted, which actually seemed to score fairly highly overall.

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

Posted 2018-12-19T08:16:03.627

Reputation: 13 242

if own_score mean + 5: gives an error for me. Also while sum(self.current_throws) – Spitemaster – 2018-12-19T17:05:20.577

@Spitemaster was an error pasting into stack exchange, should work now. – FlipTack – 2018-12-19T17:09:17.133

@Spitemaster it's because there were < and > symbols which interfered with the <pre> tags i was using – FlipTack – 2018-12-19T17:09:45.267

2

ExpectationsBot

Just plays it straight, calculates the expected value for the dice throw and only makes it if it's positive.

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

I was having trouble running the controller, got a "NameError: name 'bots_per_game' is not defined" on the multithreaded one, so really no idea how this performs.

Cain

Posted 2018-12-19T08:16:03.627

Reputation: 1 149

1I think this ends up being equivalent to a "Go to 16" bot, but we don't have one of those yet – Stuart Moore – 2018-12-19T17:47:10.273

1@StuartMoore That...is a very true point, yes – Cain – 2018-12-19T18:39:55.017

I ran into your issues with the controller when I ran it on my Windows machine. Somehow it ran fine on my Linux machine. I'm updating the controller, and will update the post once it is done. – maxb – 2018-12-19T19:47:17.457

@maxb Thanks, probably something about which variables are available in the different process. FYI also updated this, I made a silly error around yielding :/ – Cain – 2018-12-19T21:23:13.810

2

FortyTeen

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

Try for 14 points until the last round, then assume everyone else is going to try for 14 points and try to tie that score.

histocrat

Posted 2018-12-19T08:16:03.627

Reputation: 20 600

I got TypeError: unsupported operand type(s) for -: 'list' and 'int' with your bot. – tsh – 2018-12-20T03:01:08.037

I'm assuming that your max_projected_score should be the maximum of the list rather than the entire list, am I correct? Otherwise i get the same issue as tsh. – maxb – 2018-12-20T08:23:08.263

Oops, edited to fix. – histocrat – 2018-12-20T13:08:05.803

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

Dice Mastah

Posted 2018-12-19T08:16:03.627

Reputation: 21

2

Rebel

This bot combines the simple strategy of Hesitate with the advanced last round strategy of BotFor2X, tries to remember who it is and goes wild when it finds it lives in an illusion.

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

Posted 2018-12-19T08:16:03.627

Reputation: 6 366

Well this is quite elegant :) Also, congratulations on getting both first and second places in the main competition! – Dani O – 2019-01-22T21:53:09.110

Naturally I've tweaked HarkonnenBot so that Rebel can no longer unpoison itself ;) and I've also tweaked TleilaxuBot so that Rebel doesn't detect it any more! – Dani O – 2019-01-22T22:27:26.427

1

StepBot

At first I wanted to do a 4 throws bot with a gain limit of 15 but as I made it I just went wild with the coding.

Now that I notice it, it's quite bold Not so bold anymore but still a bit bold.

StepBot now enters the top ranks! Didn't think he'd made it.

Competition got really tough but I'm satisfied with the bot's results.

class StepBot(Bot):
    def __init__(self, *args):
        super().__init__(*args)
        self.cycles = 0
        self.steps = 8
        self.smallTarget = 15
        self.bigTarget = 20
        self.rush = True
        #target for game
        self.breakPoint = 40

    def make_throw(self, scores, last_round):
        # Stacks upon stacks upon stacks
        self.bigTarget += 1
        self.cycles += 1
        self.steps += 1
        if self.cycles <=3:
            self.smallTarget += 1
        else:
            self.bigTarget -= 1 if self.steps % 2 == 0 else 0

        target = self.bigTarget if scores[self.index] < 12 else self.bigTarget if self.cycles <=3 else self.smallTarget
        # If you didn't start the last round (and can't reach normally), panic ensues
        if last_round and max(scores) - (target // 3) > scores[self.index]:
            # Reaching target won't help, run for it!
            while max(scores) > scores[self.index] + sum(self.current_throws):
                yield True
        else:
            if last_round:
                self.breakPoint = max(scores)
            # Hope for big points when low, don't bite more than you can chew when high
            currentStep = 1
            while currentStep <= self.steps:
                currentStep += 1
                if sum(self.current_throws) > target:
                    break;
                yield True
                # After throw, if we get to 40 then rush (worst case we'll get drawn back)
                if scores[self.index] + sum(self.current_throws) > self.breakPoint and self.rush:
                    currentStep = 1
                    self.steps = 2
                    self.rush = False
                    target = 8 + ((random.randint(7, 15) ** 0.5) // 1)
                    # print(target)
            # If goal wasn't reached or surpassed even after rushing, run for it!
            while last_round and max(scores) > scores[self.index] + sum(self.current_throws):
                yield True
        yield False

Belhenix

Posted 2018-12-19T08:16:03.627

Reputation: 193

Thanks a lot for the heads-up – Belhenix – 2018-12-19T18:03:27.733

1I was waiting for your answer! Thanks for all the help in the sandbox. Someone still found an issue with the controller posted there, with bots casting the die one more time than they wanted. This has been fixed in the controller in the post. – maxb – 2018-12-19T19:56:02.187

1

Take Five

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

Half the time, we'll roll a 5 before a 6. When we do, cash out.

user48543

Posted 2018-12-19T08:16:03.627

Reputation:

If we stop at 1 instead, it makes slower progress, but it's more likely to get to 40 in a single bound. – None – 2018-12-19T21:08:25.593

In my tests, TakeOne got 20.868 points per round compared to TakeFive's 24.262 points per round (and also brought winrate from 0.291 to 0.259). So I don't think it's worth it. – Spitemaster – 2018-12-19T21:16:19.127

1

LeadBy5Bot

class LeadBy5Bot(Bot):
    def make_throw(self, scores, last_round):
        while True:
            current_score = scores[self.index] + sum(self.current_throws)
            score_to_beat = max(scores) + 5
            if current_score >= score_to_beat:
                break
            yield True
        yield False

Always wants to be in the lead by 5.

Edit: New Bot

RollForLuckBot

class RollForLuckBot(Bot):

    def make_throw(self, scores, last_round):
        while sum(self.current_throws) < 21:
            yield True
        score_to_beat = max([x for i,x in enumerate(scores) if i!=self.index]) + 10
        score_to_beat = max(score_to_beat, 44)
        current_score = scores[self.index] + sum(self.current_throws)
        while (last_round or (current_score >= 40)):
            current_score = scores[self.index] + sum(self.current_throws)
            if current_score > score_to_beat:
                break
            yield True
        # roll more if we're feeling lucky    
        while (random.randint(1,6) == self.current_throws[-1]):
            yield True    
        yield False

A bot that borrows from EnsureLead, I prefer using 21 as it's the average of 6d6 (6x3.5), with 6 dice rolls leaving > 70% chance of the next roll being a 6. Also, we continue to roll if we roll a separate die and match our last throw after hitting 21.

william porter

Posted 2018-12-19T08:16:03.627

Reputation: 331

Didn't notice AlphaBot till after making it. I'm curious how they'll do in a game together. – william porter – 2018-12-19T21:51:14.970

as a note, yield true should have upper T (python error) – Belhenix – 2018-12-20T00:09:55.597

@Belhenix Edited, guess I missed that when I was typing it out. – william porter – 2018-12-20T00:11:16.097

Woo, it made it into the top 50% (14 out of 28) – william porter – 2018-12-20T17:35:57.023

1

Chaser

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

Chaser tries to catch up to position one If it's the last round he desperately tries to reach at least 50 points Just for good measure he throws at least four times no matter what

[edit 1: added go-for-gold strategy in the last round]

[edit 2: updated logic because I mistakenly thought a bot would score at 40 rather than only the highest bot scoring]

[edit 3: made chaser a little more defensive in the end game]

AKroell

Posted 2018-12-19T08:16:03.627

Reputation: 133

Welcome to PPCG! Neat idea to not only try to catch up, but also pass the first place. I'm running a simulation right now, and I wish you luck! – maxb – 2018-12-20T09:50:18.790

Thanks! Initially I tried to surpass the previous leader by a fixed amount (tried values between 6 and 20) but it turns out just throwing twice more fairs better. – AKroell – 2018-12-20T10:02:50.213

@JonathanFrech thanks, fixed – AKroell – 2018-12-20T12:38:26.353

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

A pair of bots, they bring their own sets of dice and rolls them to predict the future. If one is a 6 they stop, FutureBot can't remember which of it's 2 dice was for the next roll so it gives up.

I wonder which will do better.

OneStepAhead is a little too similar to OneInFive for my taste, but I also want to see how it compares to FutureBot and OneInFive.

Edit: Now they stop after hitting 45

william porter

Posted 2018-12-19T08:16:03.627

Reputation: 331

Welcome to PPCG! Your bot definitely plays with the spirit of the game! I'll run a simulation later this evening. – maxb – 2018-12-20T16:31:54.747

Thanks! I'm curious as to how well it'll do, but I'm guessing it'll be on the low side. – william porter – 2018-12-20T16:34:21.413

1

FlipCoinRollDice

class FlipCoinRollDice(Bot):
    def make_throw(self, scores, last_round):
        while random.randint(1,2) == 2:
            throws = random.randint(1,6) != 6
            x = 0
            while x < throws:
                x = x + 1
                yield True
        yield False

This is a weird (untested) one. It flips a coin and if it's heads it rolls a dice and throws the amount the dice shows.

I can't test it now so if there are syntax errors, let me know :)

Martijn Vissers

Posted 2018-12-19T08:16:03.627

Reputation: 411

When I saw the name, I was afraid that it'd be a copy of the BlessRNG or BringMyOwn_dice bots, but this is some kind of middle ground in a way. I'm running a simulation now! Congratulations on your first KotH answer by the way! – maxb – 2018-12-21T14:37:05.037

Thank you :) I was hesitant to post it at first. It might not perform the best but it will be interesting to see. – Martijn Vissers – 2018-12-21T14:57:16.507

1Don't hesitate, just look at PointsAreForNerdsBot, it's fun to participate even if you don't win. – maxb – 2018-12-21T15:02:21.353

Rather than a syntax error it's a logic error: throws = random.randint(1,6) != 6 evaluates to a boolean instead of the random number – Belhenix – 2018-12-21T16:53:36.843

1

ClunkyChicken

class ClunkyChicken(Bot):
    def make_throw(self, scores, last_round):
        #Go for broke in last round
        if last_round:
            while scores[self.index] + sum(self.current_throws) <= max(scores):
                yield True
        #Not Endgame yet
        if scores[self.index] < (self.end_score+6):
            #Roll up to 4 more times, but stop just before forcing the last round
            for i in range(4):
                if scores[self.index] + sum(self.current_throws) < (self.end_score - 6):
                    yield True
                else:
                    break
            yield False
        #Roll 4 times - trying to force Last Round with "reasonable" score
        else:
            for i in range(4):
                yield True
        yield False

On the last round, this bot will roll until it beats the current high score - there's no reward for Second Place.

If it is within 6 of the end score (typically 40) then it will roll 3 4 times to try and set a decent target for other bots to roll 6s aiming at.

If it is without 6 of the end score, it will roll up to 3 4 times until it is within 6, and hold position there, ready for that last triple-roll burst.

24/12: Increased the rolls from 3 (42% chance of having rolled a 6) to 4 (51% chance of having rolled a six) - riskier, but I suspect my cut-off may be limiting the Bot's score.

3 rolls: Win% 19.1, Avg Score 21.17, Avg Win 45.40, Success% 45.07

WhereFourArtThouChicken

class WhereFourArtThouChicken(Bot):
    def make_throw(self, scores, last_round):
        for i in range(4):
            yield True
        yield False

Like the ClunkyChicken, this will attempt 4 rolls (plus the mandatory roll) - unlike the ClunkyChicken, it doesn't attempt to apply any logic as to whether it should stop or play things safe. (If this bot does better, I will be very disappointed xD)

Chronocidal

Posted 2018-12-19T08:16:03.627

Reputation: 571

1

Stalker

This bot tries to be within 4 points from the leader by the last round. Otherwise gets moderate gains

class Stalker(Bot):

    def make_throw(self, scores, last_round):

        # on last round pray to rng gods to beat the highest score 
        while last_round and scores[self.index] + sum(self.current_throws) <= max(scores):
            yield True 

        if last_round and scores[self.index] + sum(self.current_throws) > max(scores):
            yield False 

        # on the earlier rounds try to aim a moderate gain
        if max(scores) < 26:
            while sum(self.current_throws) < 16:
                yield True
            yield False

        # throw until 1 dice throw behind the leader
        target = max(scores) - 5

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

AgressiveStalker

This one goes aggressive if he is leading late towards the end game, otherwise stalks

class AggressiveStalker(Bot):

    def make_throw(self, scores, last_round):

        # on last round pray to rng gods to beat the highest score 
        while last_round and scores[self.index] + sum(self.current_throws) <= max(scores):
            yield True 

        if last_round and scores[self.index] + sum(self.current_throws) > max(scores):
            yield False 

        # on the earlier rounds try to aim a moderate gain
        if max(scores) < 26:
            while sum(self.current_throws) < 16:
                yield True
            yield False


        # if we are leading go for the win
        if max(scores) > 25 and max(scores) == scores[self.index]:
            while scores[self.index] + sum(self.current_throws) < 40:
                yield True
            yield False

        # if we are behind throw until 1 dice throw behind the leader
        target = max(scores) - 5

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

Ofya

Posted 2018-12-19T08:16:03.627

Reputation: 181

1Very impressive securing the fifth place with AggressiveStalker! – maxb – 2018-12-22T11:07:41.103

1

BePrepared

class BePrepared(Bot):
    ODDS = [1.0, 0.8334, 0.8056, 0.7732, 0.7354, 0.6913, 0.6398, 0.6075, 0.5744, 0.5414, 0.509, 0.4786, 0.4519, 0.426, 0.4012, 0.3778, 0.3559, 0.3354, 0.316, 0.2977, 0.2805, 0.2643, 0.249, 0.2347, 0.221, 0.2083, 0.1962, 0.1848, 0.1742, 0.1641, 0.1546, 0.1457, 0.1372, 0.1293, 0.1218, 0.1147, 0.1081, 0.1018, 0.0959, 0.0904, 0.0851, 0.0802, 0.0755, 0.0712, 0.067, 0.0631, 0.0595, 0.0561, 0.0528, 0.0498, 0.0469, 0.0442, 0.0416, 0.0392, 0.0369, 0.0348, 0.0328, 0.0309, 0.0291, 0.0274, 0.0258, 0.0243, 0.0229, 0.0216, 0.0204, 0.0192, 0.0181, 0.017, 0.0161, 0.0151, 0.0142, 0.0134, 0.0126, 0.0119, 0.0112, 0.0106, 0.0099, 0.0094, 0.0088, 0.0083, 0.0078, 0.0074, 0.007, 0.0066, 0.0062, 0.0058, 0.0055, 0.0052, 0.0049, 0.0046, 0.0043]

    def odds_of_reaching(self, target):
        if target < 0:
                return 1
        elif target > 90:
                return 0
        else:
                return self.ODDS[target]

    def odds_of_winning_with(self, target, scores):
        odds = self.odds_of_reaching(target)
        for score in scores:
                odds *= 1 - (self.odds_of_reaching(target - score + 2) )
        return odds

    def make_throw(self, scores, last_round):
        if last_round:
                gone, to_go = [sum(self.current_throws)], []
                for score in scores[:self.index]+scores[self.index+1:]:
                        delta = score - scores[self.index]
                        if score < self.end_score:
                                to_go.append(delta)
                        else:
                                gone.append(delta)
                target = max(gone)
                odds = self.odds_of_winning_with(target, to_go)
                next_odds = self.odds_of_winning_with(target+1, to_go)
                while next_odds > odds:
                        target += 1
                        odds = next_odds
                        next_odds = self.odds_of_winning_with(target+1, to_go)        
        else:
                target = min(20, self.end_score - scores[self.index] - 3)
        while sum(self.current_throws) < target:
                yield True
        if last_round or sum(self.current_throws) + scores[self.index] < self.end_score:
                yield False
        else:
                for result in self.make_throw(scores, True):
                        yield result

Targets 20, then targets 37. Once it's the last round (either because it's accidentally gone over 40 or because another bot has), gets aggressive in proportion to how many other bots are still to go and have high scores.

histocrat

Posted 2018-12-19T08:16:03.627

Reputation: 20 600

1I have updated the highscores to include your bot. I'd say it fares quite well, being in the top third. – maxb – 2018-12-22T11:00:35.810

1

BrainBot

class BrainBot(Bot):
    import numpy as np

    def  __init__(self, index, end_score):
        super().__init__(index, end_score)
        self.brain = [[[-0.1255, 0.338, 0.5265, -0.2728], [-0.2064, -1.9173, 0.1845, -0.2536], [-0.6737, -0.1334, -0.7055, 0.0797], [-0.6055, -0.0126, 0.9261, -0.603], [0.447, -0.5381, -1.7416, 0.0596], [0.1649, -0.6795, -1.1039, -0.0138], [-0.2782, -0.2005, -1.2967, -0.8073], [0.2329, -0.5591, 1.6192, -0.218]], [[0.7411, 0.3139, 0.435, 1.002, -0.3148, -0.7791, -0.6532, -0.4672, -0.4655], [0.1982, 0.3713, 0.0426, -0.9227, 1.6118, 0.9431, 0.5612, 0.1208, 0.1115]]]

    def decide(self, input_data):
        x = np.array(input_data)
        wI = 0
        for w in self.brain:
            x = [1.0 / (1 + np.exp(-el)) for el in np.dot(w, x)]
            if wI<len(self.brain)-1:
                x.append(-1)
        return np.argmax(x)

    def make_throw(self, scores, last_round):
        while True:
            oppMaxInd = -1
            oppMaxScore = 0
            for i in range(len(scores)):
                if i==self.index: continue
                if scores[i] > oppMaxScore:
                    oppMaxScore = scores[i]
                    oppMaxInd = i
            if last_round:
                yield scores[self.index]+sum(self.current_throws)<oppMaxScore+1
            else:
                s = [oppMaxScore/self.end_score,
                     scores[self.index]/self.end_score,
                     sum(self.current_throws)/self.end_score,
                     1.0 if last_round else 0.0]
                yield self.decide(s)==1

This bot has a "brain" that is given the input [highest opponent score, own score, round score, is it final round] which it multiplies by a series of matrices to obtain the resulting decision vector. Also, I added some logic for the endgame, since it seems my algorithm couldn't take that into account (although the bit about "is it last round" is given in the input).

I used an evolutionary algorithm to try to find good coefficients for the matrices. It didn't work perfectly but the bot seems to do better than a random one. I'd be very interested to see if this idea can be improved. (How to do this for example with some machine learning techniques. How could we generate training data about choices when to make throw and when not?)

ploosu2

Posted 2018-12-19T08:16:03.627

Reputation: 111

Welcome to PPCG! I'd say that this problem is suitable for a machine learning or neural net approach, though I don't have a lot of experience within the area. I guess that you could set up unsupervised learning through simulating a ton of games and looking at how the bot's decision affects its score – maxb – 2018-12-23T23:28:39.383

I looked through your bot again, and it seems like it was misbehaving. Due to the issue with the indentation, I think the methods were not registered as class methods in previous simulation. As such, your bot behaved just like the original Bot class, which put it at the bottom. With that issue fixed, your bot is behaving a lot better! I'm running a larger simulation right now, and your winrate is almost 20%. – maxb – 2018-12-27T17:27:23.430

@maxb Thanks! I was wondering why the outcome was so low :D – ploosu2 – 2018-12-27T17:55:31.730

1

GoTo20orBestBot

I've been slow to submit an answer because I've tried to analyze this as a Markov chain, looking for insights.

Several submissions have been based on looking at the expected value of another roll, which is positive if an only if your current score is less than 15. But looking at the expected value takes too narrow a view of the problem, since what you really want to do is maximize the chance that you'll beat all the other bots. To win, you're going to have to be lucky, and so it's worth also thinking about the variance of your scores.

If you're unlucky, you won't win, so the only rolls that really matter are the ones where you are moderately lucky. Under those circumstances, it makes sense to be a bit more aggressive than just rolling to a positive expected value.

And, of course, if someone else is about to win, you've got nothing left to lose, so you ought to just go for it.

I tried various versions of this bot, going to everything from 16 to 25, and this one consistently out performed the others.

In spirit, this is very similar to @tsh GoTo20Bot, but instead of going only one point higher that the current lead, I first pass the leader, and if I have fewer than 20 points in the current round, I keep rolling.

class GoTo20orBestBot(Bot):

    def make_throw(self, scores, last_round):
        # If someone's about to win, roll until you've beat them or died.
        if max(scores)>40:
            while scores[self.index] + sum(self.current_throws) <= max(scores):
                yield True     
        # If you have not already, roll at least until the expected value of a
        # roll is negative
        while sum(self.current_throws) < 20:
            yield True
        yield False

GoToSeventeenRollTenBot

This turned out to be a surprisingly successful approach in my testing. It consistently out performed both a simple "GoTo20Bot" and "Roll10Bot". Not sure why.

class GoToSeventeenRollTenBot(Bot):

    def make_throw(self, scores, last_round):
        while sum(self.current_throws) < 17:
            yield True
        for i in range(10):
            yield True
        yield False

CCB60

Posted 2018-12-19T08:16:03.627

Reputation: 159

1

Gladiolen

class Gladiolen(Bot):
    numThrows = 6

    def make_throw(self, scores, last_round):
        i = self.index

        if last_round:
            others = scores[:i] + scores[i+1:]
            target = max(others) - scores[i]
            while sum(self.current_throws) <= target:
                yield True
            yield False
        else:
            target = 33 - scores[i]
            for _ in range(self.numThrows):
                if sum(self.current_throws) >= target:
                    yield False
                yield True
            yield False

Gladiolen starts off boldly, throwing seven times in a row. But when it comes close to 40, it'll slow down, hoping for somebody else to hit 40 first. When the last_round kicks in, it is "der Tod oder die Gladiolen" again. If you don't know what that means, you should google it:)

Hermen

Posted 2018-12-19T08:16:03.627

Reputation: 31

Even if it isn't your first post, welcome to PPCG! Your bot seems to be behaving quite well, with a win rate of around 20%. I'll run a proper simulation during the night and update the scoreboard tomorrow morning. – maxb – 2018-12-30T21:16:20.780

1

The old WisdomOfCrowds is gone. The old commentary is preserved here:

This bot tries to take advantage of the skills of the other bots! In each round, it asks the current top three scorers what they would do in its situation, then goes with the majority verdict. Somewhat disappointingly, it seems to score only around the average of all other bots -- maybe the leaders' strategies aren't transferable, or maybe the fact that many bots change their plans in the last round confuses the naive majority-verdict idea.

and of course the actual code can be retrieved from the edit history.

This bot replaces it entirely, implementing a similar idea rather more cleanly (it doesn't alter the state of any of the other bots, even incidentally). Also, it doesn't bother with the majority verdict any more (too expensive), but just interrogates (a clone of) the current leader.

import copy
import operator
class TleilaxuBot(Bot):
    """
    On each roll, identify the leading bot, make a ghola from
    it, and interrogate the ghola about whether to roll again.
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.bots = None
        self.allies = {"Tleilaxu", "WisdomOfCrowds"}

    def find_bots(self):
        for f_info in inspect.stack():
            try:
                self.bots = f_info.frame.f_locals["game_bots"]
                break
            except KeyError:
                pass
            finally:
                del f_info

    def face_dancer(self, bot):
        return any([a in bot.__class__.__name__ for a in self.allies])

    def find_leader(self, scores):
        # Exclude self and allies to avoid deadly embrace!
        z = [(s, b) for s, b in zip(scores, self.bots) if not self.face_dancer(b)]
        return sorted(z, key=operator.itemgetter(0))[-1][1]

    def axolotl(self, bot):
        """
        First create a new bot that's just an empty shell, then
        duplicate the attributes of the original into the ghola.
        """
        ghola = object.__new__(bot.__class__)
        ghola.__dict__ = bot.__dict__.copy()
        for k in bot.__dict__ :
            a = getattr(bot, k)
            # Some attributes can't be (but don't need to be) deepcopied.
            try:
                setattr(ghola, k, copy.deepcopy(a))
            except:
                try:
                    setattr(ghola, k, copy.copy(a))
                except:
                    setattr(ghola, k, a)

        ghola.index = self.index   # Change the ghola's allegiance
        return ghola

    def interrogate(self, ghola, scores, last_round):
        try:
            ghola.update_state(self.current_throws[:])
            for answer in ghola.make_throw(scores, last_round):
                break
        except:
            answer = True # or False?
        return answer

    def make_throw(self, scores, last_round):
        if not self.bots:
            self.find_bots()
        tmp_scores, ghola = scores[:], self.axolotl(self.find_leader(scores))
        while True:
            yield self.interrogate(ghola, tmp_scores, last_round)

It seems to score higher than WisdomOfCrowds but of course this depends on which other bots it's competing against - it's only ever as good as the best of the rest!

Dani O

Posted 2018-12-19T08:16:03.627

Reputation: 69

Welcome to PPCG (again)! I like the style of this answer, and it is a unique tactic indeed. However, it is not allowed as it is currently presented. This is due to the rule "Any attempt to tinker with the controller, runtime or other submissions will be disqualified. All submissions should only work with the inputs and storage they are given." This rule is broken because you access the game_bots array, meaning that you call methods in instantiated bot objects that are part of the game. To circumvent this, you could create your own instance of each bot class, and use your local copy to ask... – maxb – 2018-12-31T15:10:59.400

... any question you want. The key here is that you can use other classes and call them (though you may definitely not modify them), but you may not use other objects that are part of the competition. As long as you notice that distinction, your tactic is appropriate. I noticed when I tested your bot that it made OptFor2X fail during the simulations. I don't know exactly what caused it, but removing your bot removed the error in OptFor2X. If you need any help with implementation, send me a message in the chat room, though I might not be available until tomorrow or the day after. – maxb – 2018-12-31T15:15:15.183

OptFor2X is not prepared for the situation of having a score between 1 and 13. That's usually fine, because it won't put itself into this situation! You'd need except TypeError: or just except: to catch this problem. I think most bots (including OptFor2X) will not be harmed by this bot, but it may spoil the internal state of bots like SlowStartand StepBot. – Christian Sievers – 2018-12-31T16:18:36.207

I see no reason for the yield False statement and expect it to harm the success of your bot. – Christian Sievers – 2018-12-31T17:51:37.683

@ChristianSievers: the yield False was an attempt to avoid breaking other (later) bots by calling them before they had first been called by the game itself. Also it may not really be meaningful to ask about the leaders during the first round, when some bots have not yet had a turn. So WisdonOfCrowds just accepts a single roll on its first turn; the majority-vote logic only kicks in on subsequent turns. – Dani O – 2018-12-31T19:19:18.053

@ChristianSievers: I had some exception-trapping logic in there at one point -- that's how I found that OptFor2X was breaking my code by changing scores! But having tweaked the controller to pass a copy of the scores I wasn't seeing any more exceptions, so I dropped it in this version -- obviously not enough testing, as presumably when I ran this bot it didn't happen that WisdomOfCrowds had between 1 and 13 AND OptFor2X was a leader! – Dani O – 2018-12-31T19:28:16.397

I see, but your bot might still call other bots before they have been called by the controller. I don't think that would be more problematic than calling them at all, and they'd probably give good advice: roll again! Alternatively, you could use some better trivial first round strategy like roll six times or go to 20 or whatever. (But your whole tactic relies on the assumption that leading bots are bots with good strategy, which I'd doubt. Still, they have a good chance of not being too bad, and it's an interesting attempt.) – Christian Sievers – 2018-12-31T22:10:43.247

I still haven't got this bot working. Every time I include it it still gives me the same error. Until that error is fixed, I cannot add it to the high score list. The tournament officially ends in 3 days, so there's still some time to fix the issue. – maxb – 2019-01-02T00:37:49.917

@maxb: I just added exception-trapping to suppress any and all errors in the target make_throw(), so it should at least be runnable now. Of course, it may still corrupt the internal state of the target bot but that's not my problem ;) – Dani O – 2019-01-02T10:59:05.950

All comments above may now be out of date as I've just updated the code with a completely rewritten version :D – Dani O – 2019-01-03T18:30:49.607

If you like to interrogate other people's bots, you might like this.

– Christian Sievers – 2019-01-04T15:22:21.417

The bot looks a lot better now, and making it create its own copy of each class removes any issues with disturbing the game. If I was to give some advice, you could move the logic for creating copies of bots from make_throw to __init__. That way, you can create a copy of each bot once, check which bot is in the lead, and call the respective make_throw. I'm not 100% sure that this would be faster, but I'm writing this because this bot uses up 50% of the total run time. I'll see if I can throw together a concept. – maxb – 2019-01-05T10:48:23.593

@maxb This bot is called TleilaxuBot but has only Tleilaxu in his list of allies, so it seems that its self recognition doesn't work. So when it leads, it will ask (another instance of) itself, resulting in a caught RuntimeError: maximum recursion depth exceeded. I guess this contributes a lot to its running time. – Christian Sievers – 2019-01-06T11:52:49.800

@ChristianSievers even with that fixed, there is no speedup. But I have still managed to simulate $3*10^8$ games, which is more than enough to get a fair scoring. – maxb – 2019-01-06T14:45:01.077

@ChristianSievers: any([a in bot.__class__.__name__ for a in self.allies]) is True for any bot whose name contains any element of self.allies as a substring. In local testing, I might have multiple variant instances of this bot in play, so they need to avoid each other -- otherwise I could just have checked self or self.index as in the old WisdomOfCrowds! – Dani O – 2019-01-07T08:07:14.553

Right, I forgot that in checks for substring (even though I've used that before). Anyway, it's enough to know it's not == to see that something more interesting is going on. Sorry for the confusion! – Christian Sievers – 2019-01-07T10:50:20.797

I took the liberty of trying to improve this bot, and created CopyBot. By keeping a permanent copy of each bot, and avoiding initialization in each game, I managed to make it 5 times faster, and by defaulting to throwing 5 times when an error occurs, the win rate is a bit higher.

– maxb – 2019-01-07T15:51:37.733

@maxb Your bot also talks to the other bots in the right way, so that Rebel won't notice and bots like ThrowTwiceBot won't be confused. – Christian Sievers – 2019-01-09T23:00:46.417

1

class HarkonnenBot(Bot):
    """
    House Harkonnen is unrivalled in treachery and double-dealing.
    This bot adminsters an elacca drug to all its rivals, removing
    their instinct for self-preservation and compelling them to
    obsessively roll again and again, until they *die* ⚅
    """
    def __init__(self, *args):
        super().__init__(*args)
        self.bots = None
        for f_info in inspect.stack():
            try:
                self.bots = f_info.frame.f_locals["game_bots"]
                break
            except KeyError:
                pass
            finally:
                del f_info

    def update_state(self, current_throws):                                                                                                                               
        # Do not count what you have lost. Count only what you still have.
        pass                                                                                                                                                              

    def elacca(self, scores, last_round):
        while True:
            yield True

    def chaumurky(self):
        my = self.__class__                                                                                                                                               
        for bot in self.bots:
            # Administer elacca, and defeat Rebel's antidote to it ;-)
            roll = my.make_throw if bot == self else my.elacca
            bot.make_throw = functools.partial(roll, bot)
            bot.update_state = functools.partial(my.update_state, bot)
        # Destroy the evidence
        self.bots = None

    def make_throw(self, scores, last_round):
        if self.bots:
            self.chaumurky()
        yield False

Obviously, this bot has to Ignore All The Rules, except the one that says "Sabotage is allowed, and encouraged". But when did a Harkonnen ever need encouragement? Or even permission? };D

Dani O

Posted 2018-12-19T08:16:03.627

Reputation: 69

My hippie soul likes how this turns every bot into a cooperating bot (in the sense of the cooperative swarm). If only this bot cooperated, too... – Christian Sievers – 2019-01-02T22:52:57.040

Harkonnens, cooperate? Cooperation is for children and slaves! – Dani O – 2019-01-03T15:08:13.957

Added final (post-tournament) update; now circumvents Rebel's defences and get a win rate over 95% ;-) – Dani O – 2019-01-22T22:42:49.047

1

Ro

class Ro(Bot):

    def make_throw(self, scores, last_round):
        current_score = scores[self.index]
        bonus_score = sum(self.current_throws)
        total_score = current_score + bonus_score
        score_to_beat = max([x for i,x in enumerate(scores) if i!=self.index]) + 5
        initiate_end = False

        if current_score < 33:
            target_score = 33 - current_score

        if current_score < 17:
            target_score = 17

        if current_score >= 33:
            target_score = max(45, score_to_beat) - current_score
            initiate_end = True

        if last_round:
            target_score = score_to_beat - current_score

        while bonus_score < target_score:
            yield True
            bonus_score = sum(self.current_throws)
            total_score = current_score + bonus_score

        #if we go too far on accident/luck
        if total_score >= 40 and (not initiate_end):
            yield True

        yield False

Small simple bot, we strive to hit 45 (2 points above the average win score) in 3 steps, with the last step being the smallest to minimize our risk.

william porter

Posted 2018-12-19T08:16:03.627

Reputation: 331

When this bot's score is zero, both the first and the second if condition are satisfied, and target_score is set to 33. Therefore, this bot actually follows a two step strategy. – Christian Sievers – 2019-01-09T22:38:54.290

Oh snap, I had my if statement backwards @ChristianSievers Thanks for pointing it out. – william porter – 2019-01-10T01:30:29.507

1

SafetyNetBot

class SafetyNet(Bot):
    def __init__(self, *args):
        self.previous_scores = []
        self.current_scores = []
        self.difference = []
        super().__init__(*args)

    def make_throw(self, scores, last_round):
        self.current_scores = [x for i, x in enumerate(scores)]

        if len(self.current_scores) > len(self.previous_scores):
            self.previous_scores = self.previous_scores + ([0] * (len(self.current_scores) - len(self.previous_scores)))


        self.difference = list(map(lambda x,y: x-y, self.current_scores, self.previous_scores))
        self.difference = [x for i, x in enumerate(self.difference) if x>0]
        average_throws = int((float(sum(self.difference))/float(max(1,len(self.difference))))/3.5)
        current_score = scores[self.index] + sum(self.current_throws)
        high_score = max([x for i,x in enumerate(scores) if i!=self.index])
        self.previous_scores = [x for i, x in enumerate(scores)]

        for x in range(1,average_throws-1): #we already threw once getting here
            yield True

        current_score = scores[self.index] + sum(self.current_throws)
        if last_round:
            while current_score < high_score:
                yield True
                current_score = scores[self.index] + sum(self.current_throws)

        yield False

This bot figures out the average number of rolls the bots are safely making and rolls that many times. This will be thrown off by bots that roll too much hit 40 and then fail, as well as having too few bots that safely make rolls.

william porter

Posted 2018-12-19T08:16:03.627

Reputation: 331

seems interesting but scores is not defined in __init__ – Belhenix – 2019-01-04T07:30:07.677

You could set both arrays to be empty or None in the init function, and then set the update in the beginning of make_throw only. The concept is good, but it needs some fixing before being able to participate. The tournament ends later today, but if you fix it I'll include you in the official tournament – maxb – 2019-01-04T12:54:09.600

@maxb I think I fixed it now. Let me know if it has any other issues though. – william porter – 2019-01-04T16:13:39.047

@williamporter You only update previous_scores if you reach that line, i.e. if you don't throw a 6. Not sure if that's what you intended. – Christian Sievers – 2019-01-04T16:35:07.517

@ChristianSievers it is, since I can't think of a way to get them otherwise. Looking over your bot, is update_state called before our first roll every time? – william porter – 2019-01-04T16:36:58.543

We don't get to decide about a first roll, if that isn't a six, update_state is called, then make_throw. – Christian Sievers – 2019-01-04T16:40:34.993

Ah, then it'll have to do, since I can't update previous scores if I can't do anything on a 6 at start. – william porter – 2019-01-04T16:42:53.747

Oh wait I see what you mean now, hmmm, should be fixed now. – william porter – 2019-01-04T16:44:50.720

You only keep differences that aren't 0. I wonder if you really want the negative ones. In any case, it is possible that none remain, then the bot will try to divide by zero. – Christian Sievers – 2019-01-04T17:09:56.980

Negatives are important (and they also only occur if the bot goes above 40 and fails after previously having a score) for making the bot cautious. Now, it should always divide by 1 at minimum. Still, fair point about the negatives, now I'll ignore them. – william porter – 2019-01-04T17:17:18.017

Right, I had a wrong idea how often negatives would happen. I understand you don't want to ignore them, but just adding them is probably not a good idea. Zeros and negatives don't seem very different here. – Christian Sievers – 2019-01-04T17:33:18.733

If I was to redo the challenge, I'd probably call make_throw on each round and enable players to decide if they want to throw the first time. It was an oversight on my part, but once I realized that there were too many answers to change it. I'll try the bot out and see if it works. – maxb – 2019-01-04T20:24:50.550

I figured that was the case. Thanks! – william porter – 2019-01-04T21:29:13.993

The bot works now, with one small addition. If you override __init__, you also have to manually call super().__init__(*args). I've added that to your class. – maxb – 2019-01-05T10:40:43.610

Ah thanks, still learning python, it's been a fun challenge. – william porter – 2019-01-05T19:20:38.037

@maxb I think the rule that one has to throw at least once is fine. You could still have called make_throw, and if the answer is not to throw, either ignore that or disqualify the bot. After the challenge was posted and answers relied on the specification, a non-disruptive way to let bots have extra information would have been to add methods to the Bot class with a default implementation that does nothing. – Christian Sievers – 2019-01-06T00:49:33.400

1

This is a derivative of TleilaxuBot and CopyBot; but instead of querying the leader of the current game, it clones and questions the bot with the best overall win rate so far.

import itertools
import operator
class CloneBot(Bot):
    """
    At the start of each game, identify the bot class that has
    the best win rate. If we already have a clone of it, use
    that; otherwise make one and cache it for future use. Then
    on each throw, ask the current clone whether to roll again.
    """
    _controller = None

    def _setup(self):
        # One-time initialisation (per-class-instance/per-thread)
        my = self.__class__
        my._super = my.mro()[1]
        my._controller = self._find_controller()
        my._exclude = (
                        my.__name__, "Clone", "Committee",
                        "CopyBot", "Crowd", "Ghola", "Tleilaxu",
                      )
        my._farm = {}
        my._index = { c.__name__: c for c in self._controller.bots }
        my._index[self._super.__name__] = self._super
        my._played = self._controller.played_games
        my._won = self._controller.wins

    def _find_controller(self):
        for f_info in inspect.stack():
            try:
                other = f_info.frame.f_locals["self"]
                if other.__class__.__name__ == "Controller":
                    return other
            except KeyError:
                pass
            finally:
                del f_info

    def __init__(self, *args):
        super().__init__(*args)
        if not self._controller:
            self._setup()
        self.real = self.index >= 0
        self.find_clone()

    def find_clone(self):
        # Find or create a clone of the bot with the best win rate
        leader = self.find_leading_class()
        if leader in self._farm:
            self.clone = self._farm[leader]
            self.clone.index = self.index
        else:
            self.clone = self._index[leader](self.index, self.end_score)
            self._farm[leader] = self.clone

    def find_leading_class(self):
        # Return the name of the bot class with the best win rate.
        # Exclude self and similar bots to avoid deadly embrace!
        maxrate = 0.0
        rates = [(maxrate, self._super.__name__)]
        if self.real:
            for botname, won in self._won.items():
                if not any([a in botname for a in self._exclude]):
                    winrate = won/max(1, self._played[botname])
                    if winrate >= maxrate:
                        maxrate = winrate
                        rates.append((winrate, botname))
        rates = itertools.filterfalse(lambda r: r[0] < maxrate, rates)
        return sorted(rates, key=operator.itemgetter(0))[-1][-1]

    def update_state(self, curr_throws):
        super().update_state(curr_throws)
        self.clone.update_state(curr_throws)

    def make_throw(self, scores, last_round):
        try:
            for ans in self.clone.make_throw(scores, last_round):
                yield ans
        except Exception:
            yield False

The perhaps-surprising thing is that this actually beats every other bot in the main competition - even OptFor2X! Not by very much, but over the sufficiently long term (runs of more than ~1 million simulations) it usually beats OptFor2X by about 0.2%. Which seems odd, because it's mostly just getting its decisions from its own clone of OptFor2X! Here's an example run:

$ nice -20 python3 ./forty_game_controller.py -t 4 -n 50000000 -b 8 -A  

Starting simulation with 52 bots  
Simulating 50000000 games with 8 bots per game  
Running simulation on 4 threads  

[==================================================] 100%  
[==================================================] 100%  
[==================================================] 100%  
[==================================================] 100%  

Simulation or 50000000 games between 52 bots completed in 18358.5 seconds  
Each game lasted for an average of 3.80 rounds  
4972153 games were tied between two or more bots (9.94%)  
0 games ran until the round limit, highest round was 18  

+-----------------------+----+--------+--------+------+------+-------+------+--------+  
|Bot                    |Win%|    Wins|  Played|   Max|   Avg|Avg win|Throws|Success%|  
+-----------------------+----+--------+--------+------+------+-------+------+--------+  
|CloneBot               |22.4| 1724957| 7696253|   101| 20.82|  44.32|  4.00|   33.31|  
|OptFor2X               |22.2| 1705414| 7696855|    98| 20.80|  44.36|  4.00|   33.35|  
|StepBot                |21.0| 1614765| 7690916|    91| 20.75|  43.40|  4.55|   24.17|  
|Rebel                  |20.9| 1610273| 7691858|    96| 21.69|  44.22|  3.88|   35.29|  
|EnsureLead             |20.8| 1602774| 7690527|    89| 20.75|  44.07|  4.49|   25.14|  
|Hesitate               |20.8| 1596899| 7691164|    99| 21.78|  44.17|  3.88|   35.41|  
|Roll6Timesv2           |20.6| 1585827| 7697765|    98| 21.18|  43.47|  4.36|   27.29|  
|BinaryBot              |20.5| 1579192| 7688993|    97| 21.35|  44.43|  3.82|   36.25|  
|AggressiveStalker      |20.4| 1570588| 7694504|    98| 20.81|  44.78|  3.88|   35.38|  
|QuotaBot               |20.4| 1568508| 7692304|    91| 20.22|  44.93|  4.50|   25.07|  
|FooBot                 |20.4| 1566110| 7692261|   113| 22.37|  43.71|  3.90|   34.96|  
|AdaptiveRoller         |20.3| 1559090| 7694793|   107| 21.03|  43.20|  4.51|   24.91|  
|BePrepared             |20.2| 1555711| 7696658|   101| 18.88|  47.67|  4.30|   28.40|  
|GoTo20Bot              |20.2| 1552725| 7695252|    90| 21.47|  43.17|  4.44|   26.07|  
|GoTo20orBestBot        |19.9| 1532451| 7689005|    95| 21.29|  44.02|  4.45|   25.80|  
|LastRound              |19.9| 1532052| 7691686|    96| 20.83|  43.42|  4.18|   30.28|  
|BrainBot               |19.9| 1527461| 7687387|    92| 19.55|  45.54|  4.45|   25.84|  
|Gladiolen              |19.8| 1526774| 7694652|    98| 20.48|  45.26|  3.89|   35.19|  
|Stalker                |19.8| 1522105| 7691552|    97| 20.56|  45.28|  3.78|   37.06|  
|ClunkyChicken          |19.4| 1494374| 7692268|    95| 21.48|  45.40|  3.55|   40.85|  
|FortyTeen              |19.2| 1479323| 7687913|    93| 21.24|  46.73|  3.87|   35.48|  
|Chaser                 |19.0| 1463357| 7689362|    91| 19.88|  45.58|  4.04|   32.69|  
|Crush                  |19.0| 1462214| 7688887|    98| 14.98|  43.02|  5.16|   14.05|  
|Ro                     |17.4| 1340309| 7689224|   106| 19.64|  49.23|  4.09|   31.77|  
|MatchLeaderBot         |16.9| 1301503| 7692455|   105| 18.96|  45.00|  3.17|   47.24|  
|RollForLuckBot         |16.6| 1280087| 7691043|   100| 17.60|  50.45|  4.72|   21.38|  
|TakeFive               |16.5| 1272700| 7696365|    95| 19.72|  44.61|  3.35|   44.19|  
|CopyBot                |15.7| 1204247| 7691305|   103| 18.89|  45.44|  3.80|   36.69|  
|Alpha                  |15.7| 1204720| 7695029|    96| 17.79|  46.63|  4.02|   32.93|  
|GoHomeBot              |15.6| 1200452| 7693064|    44| 13.39|  41.41|  5.49|    8.51|  
|LeadBy5Bot             |15.4| 1185736| 7692771|    97| 17.52|  46.85|  4.11|   31.56|  
|NotTooFarBehindBot     |15.4| 1183868| 7694948|    92| 18.12|  45.02|  2.97|   50.50|  
|GoToSeventeenRollTenBot|14.3| 1102743| 7689943|   101| 10.41|  49.24|  5.68|    5.42|  
|LizduadacBot           |14.2| 1094300| 7694015|    84|  9.81|  51.35|  5.72|    4.68|  
|BringMyOwn_dice        |12.6|  971094| 7693980|    44| 21.59|  41.47|  4.24|   29.36|  
|SafetyNet              |11.4|  880288| 7692264|    91| 15.83|  44.99|  2.35|   60.76|  
|WhereFourArtThouChicken|10.9|  837058| 7693152|    64| 22.65|  47.39|  3.59|   40.19|  
|ExpectationsBot        | 9.6|  734632| 7691870|    44| 24.72|  41.54|  3.57|   40.45|  
|OneStepAheadBot        | 8.8|  675539| 7690564|    50| 18.51|  46.01|  3.20|   46.61|  
|GoBigEarly             | 6.9|  531024| 7690540|    49| 21.06|  42.94|  3.89|   35.22|  
|OneInFiveBot           | 6.0|  463188| 7689148|   150| 17.49|  49.69|  3.00|   50.00|  
|ThrowThriceBot         | 4.3|  330265| 7693149|    54| 22.00|  44.54|  2.53|   57.87|  
|FutureBot              | 4.3|  328789| 7688116|    50| 18.21|  45.15|  2.36|   60.71|  
|GamblersFallacy        | 1.4|  109916| 7691758|    44| 22.84|  41.46|  2.80|   53.30|  
|FlipCoinRollDice       | 0.8|   59338| 7695670|    83| 15.56|  44.54|  1.61|   73.19|  
|CooperativeThrowTwice  | 0.6|   48174| 7693657|    49| 17.05|  43.15|  2.14|   64.30|  
|BlessRNG               | 0.2|   13258| 7693434|    49| 14.80|  42.71|  1.42|   76.40|  
|StopBot                | 0.0|     212| 7694242|    44| 11.12|  41.70|  1.00|   83.34|  
|CooperativeSwarmBot    | 0.0|     196| 7694437|    44| 10.29|  41.45|  1.37|   77.18|  
|CooperativeSwarm_1234  | 0.0|     158| 7687573|    44| 10.30|  41.53|  1.37|   77.19|  
|SlowStart              | 0.0|       0| 7692021|    31|  5.29|   0.00|  3.16|   47.30|  
|PointsAreForNerdsBot   | 0.0|       0| 7691448|     0|  0.00|   0.00|  6.00|    0.00|  
+-----------------------+----+--------+--------+------+------+-------+------+--------+  

+----------+-----+  
|Percentile|Score|  
+----------+-----+  
|     50.00|   44|  
|     75.00|   47|  
|     90.00|   51|  
|     95.00|   53|  
|     99.00|   58|  
|     99.90|   67|  
|     99.99|  150|  
+----------+-----+  

+-----------------------+-------+-----+  
|Bot                    |   Time|Time%|  
+-----------------------+-------+-----+  
|BrainBot               |7404.15| 15.3|  
|CopyBot                |2511.08|  5.2|  
|CloneBot               |1225.01|  2.5|  
|BePrepared             |1092.93|  2.3|  
|SafetyNet              |1072.67|  2.2|  
|OptFor2X               |1042.43|  2.2|  
|PointsAreForNerdsBot   | 966.03|  2.0|  
|LizduadacBot           | 958.76|  2.0|  
|Rebel                  | 943.04|  1.9|  
|Crush                  | 897.89|  1.9|  
|GoToSeventeenRollTenBot| 897.28|  1.9|  
|BringMyOwn_dice        | 892.27|  1.8|  
|StepBot                | 889.61|  1.8|  
|Ro                     | 881.75|  1.8|  
|EnsureLead             | 873.70|  1.8|  
|QuotaBot               | 869.99|  1.8|  
|RollForLuckBot         | 861.64|  1.8|  
|GoHomeBot              | 859.09|  1.8|  
|OneStepAheadBot        | 842.69|  1.7|  
|LeadBy5Bot             | 814.80|  1.7|  
|Chaser                 | 810.62|  1.7|  
|FutureBot              | 809.73|  1.7|  
|Alpha                  | 792.04|  1.6|  
|NotTooFarBehindBot     | 791.59|  1.6|  
|LastRound              | 788.76|  1.6|  
|Roll6Timesv2           | 767.75|  1.6|  
|GoTo20Bot              | 765.86|  1.6|  
|OneInFiveBot           | 764.31|  1.6|  
|GoTo20orBestBot        | 763.44|  1.6|  
|BinaryBot              | 763.02|  1.6|  
|AdaptiveRoller         | 762.56|  1.6|  
|AggressiveStalker      | 757.35|  1.6|  
|Gladiolen              | 753.05|  1.6|  
|Stalker                | 752.02|  1.6|  
|ExpectationsBot        | 742.51|  1.5|  
|ClunkyChicken          | 741.84|  1.5|  
|FooBot                 | 741.44|  1.5|  
|FortyTeen              | 725.43|  1.5|  
|MatchLeaderBot         | 722.53|  1.5|  
|Hesitate               | 710.54|  1.5|  
|GoBigEarly             | 708.21|  1.5|  
|TakeFive               | 665.17|  1.4|  
|GamblersFallacy        | 645.57|  1.3|  
|SlowStart              | 641.37|  1.3|  
|WhereFourArtThouChicken| 636.17|  1.3|  
|FlipCoinRollDice       | 585.46|  1.2|  
|CooperativeThrowTwice  | 507.59|  1.0|  
|ThrowThriceBot         | 496.03|  1.0|  
|BlessRNG               | 435.49|  0.9|  
|CooperativeSwarm_1234  | 398.50|  0.8|  
|CooperativeSwarmBot    | 397.98|  0.8|  
|StopBot                | 299.57|  0.6|  
+-----------------------+-------+-----+  

Dani O

Posted 2018-12-19T08:16:03.627

Reputation: 69

This is very impressive! Going for the strategy of the overall winner sounds a lot more stable than copying the leader of the current round. However, the fact that it beats every other competitor while relying on their strategies is remarkable. This is definitely one of my favorite bots. While it could be argued that it will copy the same bot 99.99% of the time, which would almost make it a copy of that bot, the idea is still unique and performant at the same time. Good job! – maxb – 2019-02-12T12:01:01.187

0

GamblersFallacy

class GamblersFallacy(Bot):
    def make_throw(self, scores, last_round):
        # since we're guaranteed to throw once, only throw up to 4 extra times
        for i in range(4):
            # the closer the score gets to winning,
            # and the closer the throws get to equaling 5,
            # the more likely the bot is to quit
            if (scores[self.index]/40.0 + len(self.current_throws)/5.0) < 0.90:
                yield True
            else:
                break
        yield False

This Bot believes the following to be true:

  1. I'm less likely to get a 6 if I roll fewer than 6 times.
  2. I'm more likely to get a 6 the closer I am to winning.
  3. I'm more likely to get a 6 the closer I am to my limit of 6 throws.

It acts accordingly, only rolling extra when it thinks it is safe to do so.

Triggernometry

Posted 2018-12-19T08:16:03.627

Reputation: 765

0

MatchLeaderBot

class MatchLeaderBot(Bot):
    # Try to match the current leader, then pass them in the last round
    def make_throw(self, scores, last_round):

        while True:
            current_top = max(scores)
            my_total = scores[self.index]
            my_round_total = sum(self.current_throws)
            my_current_total = my_total + my_round_total
            difference = current_top - my_current_total

            if last_round and my_current_total < current_top:
                # Go for gold while we still can
                yield True
                continue
            elif difference > 5:
                # Don't risk another throw if we could pass leader in one toss
                yield True
                continue
            break

        yield False

This bot attempts to get as close as possible to the current leader's score, then tries to pass them in the last round. It won't take the risk if one more toss could put it in the lead - unless it's the last round.

Still a work in progress as I'm a bit of a Python noob.

Cory Gehr

Posted 2018-12-19T08:16:03.627

Reputation: 1

0

Crush

class Crush(Bot):
    def make_throw(self, scores, last_round):
        # 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 

        # If no one is close enough, claim victory. 
        if max(scores[:self.index] + scores[self.index + 1:]) < self.end_score - 10:
            while scores[self.index] + sum(self.current_throws) < self.end_score:
                yield True
            yield False 

        # Otherwise, play safe and wait for someone else to cross the finish line.
        if scores[self.index] <= 20:
            while scores[self.index] + sum(self.current_throws) < 20:
                yield True
            yield False
        if scores[self.index] <= 35:
            while scores[self.index] + sum(self.current_throws) < 35:
                yield True
            yield False
        while True:
            yield True

Only go for the win if no one else is close enough to stop us. There's certainly a lot of improvement to be had here, so I'll probably optimize it further, but it does pretty well as is.

user48543

Posted 2018-12-19T08:16:03.627

Reputation:

0

OptFor2X is causing a problem; this bit of code:

def make_throw(self,scores,last_round):
    myscore=scores[self.index]
    if last_round:
        target=max(scores)-myscore
        opscores = []
        scores += scores

changes the controller's game_scores[] array so it doesn't any more match the game_bots[] array.

To prevent accidental (or deliberate) damage of this type, the controller should probably pass a copy of the game_scores to each bot, as it does with the current_throws[] array:

bot.update_state(current_throws[:])                                       
for throw in bot.make_throw(game_scores[:], last_round): 

Or for greater efficiency, make the copy outside the for-loop:

bot.update_state(current_throws[:])
tmp_scores = game_scores[:]                                       
for throw in bot.make_throw(tmp_scores, last_round): 

Enjoy,
:D

Dani O

Posted 2018-12-19T08:16:03.627

Reputation: 69

1Welcome to the site. I know you can't comment but this type of thing should be left as a comment because it is not an answer. – Post Rock Garf Hunter – 2018-12-30T13:11:25.870

Well spotted! I agree that the controller should not allow this, but changed my bot anyway. It doesn't seem to me that your two versions would really perform differently. – Christian Sievers – 2018-12-30T15:10:24.663

Welcome to PPCG! You're very right in that each bot should only receive a copy of the state instead of direct references. I'll look into that. For the future, an issue like this is best suited for the chat room related to this challenge, you can ping the author of the challenge (me) if they're slow to answer. I don't think the modification was deliberate, but it should not be possible to do that. – maxb – 2018-12-30T19:38:31.333

Also for the sake of efficiency, the two versions perform equally, since the make_throw function is only called once, as an iterator. Though I think that the second version is more readable, but this site is not great on readable code in general... – maxb – 2018-12-30T21:18:05.213

@Wît Wisarhd: yes, I tried to submit it as a comment about OptFor2X, but as a new contributor I'm not allowed to comment on other people's posts! – Dani O – 2018-12-30T23:16:19.450

@maxb: the efficiency thing -- I was thinking it looks like make_throw() is called repeatedly, so the scores[:] would be copied each time; but of course it's not a new call each time through the loop :) In which case using the scores[:] notation maybe has the advantage of making it more like the update_state() call? – Dani O – 2018-12-30T23:23:53.530

0

CopyBot

This bot is basically an updated version of TleilaxuBot. It keeps its own copies of all bots, and asks the current leader for advice. If the leader is unable to answer, it defaults to throwing the die 5 times. It achieves a win rate of about 16% using this strategy.

class CopyBot(Bot):
    _bot_copies = {}
    _avoid = set(['TleilaxuBot', 'CopyBot'])

    def __init__(self, *args):
        super().__init__(*args)

        if not self._bot_copies:
            for f_info in inspect.stack():
                try:
                    all_bots = f_info.frame.f_locals["bots"]
                    break
                except KeyError:
                    pass
                finally:
                    del f_info
            for bot_class in all_bots:
                name = bot_class.__name__
                if name != __class__.__name__:
                    self._bot_copies[name] = bot_class(-1, self.end_score)

        for bot in self._bot_copies.values():
            bot.index = self.index
        self.find_bots()

    def find_bots(self):
        for f_info in inspect.stack():
            try:
                self.bots = f_info.frame.f_locals["game_bots"]
                self.bot_names = [bot.__class__.__name__ for bot in self.bots]
                break
            except KeyError:
                pass
            finally:
                del f_info

    def update_state(self, current_throws):
        self.bot_names = [bot.__class__.__name__ for bot in self.bots]
        self.current_throws = current_throws
        for i, bot_name in enumerate(self.bot_names):
            if i != self.index:
                self._bot_copies[bot_name].update_state(current_throws)

    def make_throw(self, scores, last_round):
        self.bot_names = [bot.__class__.__name__ for bot in self.bots]
        other_scores = [
            (s, i) for i, s in enumerate(scores) 
            if self.bot_names[i] not in self._avoid
        ]
        ind = [i for s, i in other_scores if s == max(other_scores)[0]][0]

        throws = 0
        try:
            winner_bot = self._bot_copies[self.bot_names[ind]]
            for answer in winner_bot.make_throw(scores, last_round):
                throws += 1
                yield answer
                winner_bot.update_state(self.current_throws)
        except Exception:
            while throws < 5:
                yield True
                throws += 1
            yield False

maxb

Posted 2018-12-19T08:16:03.627

Reputation: 5 754

That's certainly more efficient. So it's inspired me to write another variant that's even more efficient. I started by creating my own bots as and when needed, rather than creating all of them at start of day. Then I optimised the code to find the leader, and stripped down the update_state() code so that it only updated the bot we were using (this might break if bots kept track of state updates, but AFAICS there aren't any that do). All this got the CPU time down to about one-tenth of TleilaxuBot or half that of CopyBot , but the win rate still wasn't any better ... – Dani O – 2019-02-11T22:49:53.473

Then I decided to change the strategy a little; instead of asking the bot that's in the lead in the current game, my new variant CloneBot asks its own instance of the bot that's got the best overall win rate ... with remarkable results :) – Dani O – 2019-02-11T22:54:40.957