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.
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 yield
ed 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|
+----------+-----+
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 useBot.__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.0001@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.397How 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 includeand not last_round
, otherwise you may reassign thelast_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