Turnier vorbei!
Das Turnier ist jetzt vorbei! Die letzte Simulation wurde während der Nacht durchgeführt, insgesamt Spiele. Der Gewinner ist Christian Sievers mit seinem Bot OptFor2X . Auch Christian Sievers konnte sich mit Rebel den zweiten Platz sichern . Herzliche Glückwünsche! Unten sehen Sie die offizielle Highscore-Liste für das Turnier.
Wenn Sie das Spiel dennoch spielen möchten, können Sie gerne den unten angegebenen Controller verwenden und den Code darin verwenden, um Ihr eigenes Spiel zu erstellen.
Ich wurde eingeladen, ein Würfelspiel zu spielen, von dem ich noch nie gehört hatte. Die Regeln waren einfach, aber ich denke, es wäre perfekt für eine KotH-Herausforderung.
Die Regeln
Der Beginn des Spiels
Der Würfel geht um den Tisch und jedes Mal, wenn Sie an der Reihe sind, können Sie den Würfel so oft werfen, wie Sie möchten. Sie müssen es jedoch mindestens einmal werfen. Sie behalten die Summe aller Würfe für Ihre Runde im Auge. Wenn Sie aufhören möchten, wird die Punktzahl für die Runde zu Ihrer Gesamtpunktzahl hinzugefügt.
Warum würdest du jemals aufhören, den Würfel zu werfen? Denn wenn Sie 6 bekommen, wird Ihre Punktzahl für die gesamte Runde Null und der Würfel wird weitergereicht. Daher ist das ursprüngliche Ziel, Ihre Punktzahl so schnell wie möglich zu erhöhen.
Wer ist der Gewinner?
Wenn der erste Spieler am Tisch 40 Punkte oder mehr erreicht, beginnt die letzte Runde. Sobald die letzte Runde begonnen hat, erhält jeder außer der Person, die die letzte Runde eingeleitet hat, eine weitere Runde.
Die Regeln für die letzte Runde sind die gleichen wie für jede andere Runde. Sie entscheiden sich dafür, weiter zu werfen oder aufzuhören. Sie wissen jedoch, dass Sie keine Gewinnchance haben, wenn Sie in der letzten Runde keine höhere Punktzahl erzielen als vor Ihnen. Aber wenn Sie zu weit gehen, erhalten Sie möglicherweise eine 6.
Es gibt jedoch noch eine weitere Regel, die berücksichtigt werden muss. Wenn Ihre aktuelle Gesamtpunktzahl (Ihre vorherige Punktzahl + Ihre aktuelle Punktzahl für die Runde) 40 oder mehr beträgt und Sie eine 6 treffen, wird Ihre Gesamtpunktzahl auf 0 gesetzt. Das bedeutet, dass Sie von vorne beginnen müssen. Wenn Sie eine 6 erreichen, wenn Ihre aktuelle Gesamtpunktzahl 40 oder mehr beträgt, wird das Spiel normal fortgesetzt, mit der Ausnahme, dass Sie jetzt auf dem letzten Platz sind. Die letzte Runde wird nicht ausgelöst, wenn Ihre Gesamtpunktzahl zurückgesetzt wird. Sie könnten die Runde noch gewinnen, aber es wird schwieriger.
Der Gewinner ist der Spieler mit der höchsten Punktzahl, wenn die letzte Runde vorbei ist. Wenn zwei oder mehr Spieler die gleiche Punktzahl haben, werden sie alle als Sieger gewertet.
Eine zusätzliche Regel ist, dass das Spiel maximal 200 Runden lang fortgesetzt wird. Dies soll verhindern, dass mehrere Bots grundsätzlich so lange werfen, bis sie 6 treffen, um auf ihrem aktuellen Stand zu bleiben. Sobald die 199. Runde vorbei ist, last_round
wird auf true gesetzt und eine weitere Runde gespielt. Wenn das Spiel bis zu 200 Runden dauert, gewinnt der Bot (oder die Bots) mit der höchsten Punktzahl, auch wenn sie nicht über 40 Punkte oder mehr verfügen.
Rekapitulieren
- In jeder Runde wirfst du den Würfel, bis du aufhörst oder eine 6 erhältst
- Du musst den Würfel einmal werfen (wenn dein erster Wurf eine 6 ist, ist deine Runde sofort vorbei)
- Wenn Sie eine 6 erhalten, wird Ihre aktuelle Punktzahl auf 0 gesetzt (nicht Ihre Gesamtpunktzahl).
- Sie addieren Ihre aktuelle Punktzahl nach jeder Runde zu Ihrer Gesamtpunktzahl
- Wenn ein Bot seinen Zug beendet und eine Gesamtpunktzahl von mindestens 40 erreicht, erhält jeder andere eine letzte Runde
- Wenn Ihre aktuelle Gesamtpunktzahl und Sie eine 6 erhalten, wird Ihre Gesamtpunktzahl auf 0 gesetzt und Ihre Runde ist beendet
- Die letzte Runde wird nicht ausgelöst, wenn die oben genannten Ereignisse eintreten
- Die Person mit der höchsten Gesamtpunktzahl nach der letzten Runde ist der Gewinner
- Falls es mehrere Gewinner gibt, werden alle als Gewinner gewertet
- Das Spiel dauert maximal 200 Runden
Klärung der Partituren
- Gesamtpunktzahl: Die Punktzahl, die Sie in früheren Runden gespeichert haben
- Aktuelle Punktzahl: Die Punktzahl für die aktuelle Runde
- Aktuelle Gesamtpunktzahl: Die Summe der beiden obigen Punkte
Wie nimmst du teil?
Um an dieser KotH-Herausforderung teilzunehmen, sollten Sie eine Python-Klasse schreiben, von der erbt Bot
. Sie sollten die Funktion implementieren: make_throw(self, scores, last_round)
. Diese Funktion wird aufgerufen, sobald Sie an der Reihe sind und Ihr erster Wurf keine 6 war. Um weiter zu werfen, sollten Sie yield True
. Um mit dem Werfen aufzuhören, solltest du yield False
. Nach jedem Wurf wird die übergeordnete Funktion update_state
aufgerufen. Somit haben Sie über die Variable Zugriff auf Ihre Würfe für die aktuelle Runde self.current_throws
. Sie haben auch Zugriff auf Ihren eigenen Index mit self.index
. So, um Ihre eigene Gesamtpunktzahl zu sehen, würden Sie verwenden scores[self.index]
. Sie können auch end_score
mithilfe von auf das für das Spiel zugreifen self.end_score
, aber Sie können davon ausgehen, dass es für diese Herausforderung 40 sein wird.
Sie dürfen Hilfsfunktionen in Ihrer Klasse erstellen. Sie können auch in der Bot
übergeordneten Klasse vorhandene Funktionen überschreiben , z. B. wenn Sie weitere Klasseneigenschaften hinzufügen möchten. Sie dürfen den Status des Spiels in keiner Weise ändern, außer in Bezug auf "Nachgeben" True
oder " Nachgeben" False
.
Es steht Ihnen frei, sich von diesem Beitrag inspirieren zu lassen und einen der beiden Bots zu kopieren, die ich hier aufgenommen habe. Ich fürchte jedoch, dass sie nicht besonders effektiv sind ...
Zum Zulassen anderer Sprachen
Sowohl in der Sandbox als auch in The Nineteenth Byte haben wir Diskussionen darüber geführt, ob Beiträge in anderen Sprachen eingereicht werden dürfen. Nachdem ich über solche Implementierungen gelesen und Argumente von beiden Seiten gehört habe, habe ich beschlossen, diese Herausforderung nur auf Python zu beschränken. Dies ist auf zwei Faktoren zurückzuführen: die für die Unterstützung mehrerer Sprachen erforderliche Zeit und die Zufälligkeit dieser Herausforderung, die eine hohe Anzahl von Iterationen erfordert, um Stabilität zu erreichen. Ich hoffe, dass Sie weiterhin teilnehmen und wenn Sie Python für diese Herausforderung lernen möchten, werde ich versuchen, so oft wie möglich im Chat verfügbar zu sein.
Bei Fragen, die Sie möglicherweise haben, können Sie im Chat-Raum für diese Herausforderung schreiben . Wir sehen uns dort!
Regeln
- Sabotage ist erlaubt und wird empfohlen. Das heißt, Sabotage gegen andere Spieler
- Jeder Versuch, mit dem Controller, der Laufzeit oder anderen Einsendungen zu basteln, wird disqualifiziert. Alle Einsendungen sollten nur mit den angegebenen Eingaben und Speichern funktionieren.
- Jeder Bot, der mehr als 500 MB Speicher benötigt, um seine Entscheidung zu treffen, wird disqualifiziert (wenn Sie so viel Speicher benötigen, sollten Sie Ihre Auswahl überdenken).
- Ein Bot darf nicht absichtlich oder versehentlich die exakt gleiche Strategie wie ein vorhandener implementieren.
- Sie dürfen Ihren Bot während der Dauer der Challenge aktualisieren. Sie können jedoch auch einen anderen Bot posten, wenn Ihre Herangehensweise anders ist.
Beispiel
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
Dieser Bot wird so lange weitermachen, bis er mindestens 10 Punkte für die Runde erzielt hat oder eine 6 auslöst. Beachten Sie, dass Sie keine Logik mehr benötigen, um 6 zu werfen. Beachten Sie auch, dass, wenn Ihr erster Wurf eine 6 make_throw
ist, nie angerufen, da deine Runde sofort vorbei ist.
Für diejenigen, die neu in Python (und neu im yield
Konzept) sind, dies aber ausprobieren möchten, yield
ähnelt das Schlüsselwort in gewisser Weise einer Rendite, in anderer Hinsicht unterscheidet es sich jedoch. Über das Konzept können Sie hier lesen . Grundsätzlich yield
stoppt Ihre Funktion , sobald Sie dies tun, und der von Ihnen yield
eingegebene Wert wird an die Steuerung zurückgesendet. Dort verarbeitet der Controller seine Logik, bis es Zeit für Ihren Bot ist, eine andere Entscheidung zu treffen. Dann sendet der Controller Ihnen den Würfelwurf und Ihre make_throw
Funktion wird genau dort ausgeführt, wo sie zuvor gestoppt wurde, im Grunde in der Zeile nach der vorherigen yield
Anweisung.
Auf diese Weise kann der Gamecontroller den Status aktualisieren, ohne dass für jeden Würfelwurf ein separater Bot-Funktionsaufruf erforderlich ist.
Spezifikation
Sie können jede Python-Bibliothek verwenden, die in verfügbar ist pip
. Um sicherzustellen, dass ich einen guten Durchschnitt erhalten kann, haben Sie ein Zeitlimit von 100 Millisekunden pro Runde. Ich würde mich sehr freuen, wenn Ihr Skript viel schneller wäre, damit ich mehr Runden drehen kann.
Auswertung
Um den Gewinner zu finden, nehme ich alle Bots und starte sie in zufälligen 8er-Gruppen. Wenn weniger als 8 Klassen eingereicht werden, starte ich sie in zufälligen 4er-Gruppen, um zu vermeiden, dass immer alle Bots in jeder Runde sind. Ich werde ungefähr 8 Stunden lang Simulationen durchführen, und der Gewinner wird der Bot mit dem höchsten Gewinnanteil sein. Ich starte die letzten Simulationen Anfang 2019 und gebe dir Weihnachten, um deine Bots zu programmieren! Das vorläufige Enddatum ist der 4. Januar, aber wenn das zu wenig Zeit ist, kann ich es auf ein späteres Datum umstellen.
Bis dahin werde ich versuchen, eine tägliche Simulation mit 30-60 Minuten CPU-Zeit durchzuführen und die Anzeigetafel zu aktualisieren. Dies wird nicht die offizielle Punktzahl sein, aber es wird als Leitfaden dienen, um zu sehen, welche Bots die beste Leistung erbringen. Ich hoffe jedoch, dass Sie mit Weihnachten verstehen können, dass ich nicht immer verfügbar sein werde. Ich werde mein Bestes geben, um Simulationen durchzuführen und alle Fragen im Zusammenhang mit der Herausforderung zu beantworten.
Testen Sie es selbst
Wenn Sie Ihre eigenen Simulationen ausführen möchten, finden Sie hier den vollständigen Code des Controllers, der die Simulation ausführt, einschließlich zweier Beispiel-Bots.
Regler
Hier ist der aktualisierte Controller für diese Herausforderung. Es unterstützt ANSI-Ausgänge, Multithreading und sammelt dank AKroell zusätzliche Statistiken ! Wenn ich Änderungen am Controller vornehme, aktualisiere ich den Post, sobald die Dokumentation vollständig ist.
Dank BMO kann der Controller nun alle Bots von diesem Beitrag mit der -d
Flagge herunterladen . Andere Funktionen bleiben in dieser Version unverändert. Dies sollte sicherstellen, dass alle Ihre letzten Änderungen so schnell wie möglich simuliert werden!
#!/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)
Wenn Sie für diese Herausforderung auf den ursprünglichen Controller zugreifen möchten, ist dieser im Bearbeitungsverlauf verfügbar. Der neue Controller verfügt über genau die gleiche Logik für die Ausführung des Spiels. Der einzige Unterschied besteht in der Leistung, der Erfassung von Statistiken und dem hübscheren Drucken.
Bots
Auf meinem Computer werden die Bots in der Datei gespeichert forty_game_bots.py
. Wenn Sie einen anderen Namen für die Datei verwenden, müssen Sie die import
Anweisung oben auf dem Controller aktualisieren .
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
Simulation ausführen
Speichern Sie zum Ausführen einer Simulation beide oben veröffentlichten Codefragmente in zwei separaten Dateien. Ich habe sie als forty_game_controller.py
und gespeichert forty_game_bots.py
. Dann benutzt du einfach python forty_game_controller.py
oder python3 forty_game_controller.py
abhängig von deiner Python-Konfiguration. Befolgen Sie die Anweisungen von dort, wenn Sie Ihre Simulation weiter konfigurieren möchten, oder versuchen Sie, den Code zu basteln, wenn Sie möchten.
Spielstatistik
Wenn Sie einen Bot erstellen, der auf eine bestimmte Punktzahl abzielt, ohne andere Bots zu berücksichtigen, sind dies die Perzentile der Siegpunktzahl:
+----------+-----+
|Percentile|Score|
+----------+-----+
| 50.00| 44|
| 75.00| 48|
| 90.00| 51|
| 95.00| 54|
| 99.00| 58|
| 99.90| 67|
| 99.99| 126|
+----------+-----+
Highscores
Wenn weitere Antworten veröffentlicht werden, werde ich versuchen, diese Liste auf dem neuesten Stand zu halten. Der Inhalt der Liste ist immer von der neuesten Simulation. Die Bots ThrowTwiceBot
und GoToTenBot
sind die Bots aus dem obigen Code und werden als Referenz verwendet. Ich habe eine Simulation mit 10 ^ 8 Spielen gemacht, die ungefähr 1 Stunde gedauert hat. Dann sah ich, dass das Spiel im Vergleich zu meinen Läufen mit 10 ^ 7 Spielen Stabilität erreichte. Da die Leute immer noch Bots posten, werde ich keine Simulationen mehr durchführen, bis die Häufigkeit der Antworten gesunken ist.
Ich versuche, alle neuen Bots hinzuzufügen und alle Änderungen, die Sie an vorhandenen Bots vorgenommen haben, hinzuzufügen. Wenn es den Anschein hat, dass ich Ihren Bot verpasst habe oder Sie neue Änderungen vorgenommen haben, schreiben Sie in den Chat und ich werde sicherstellen, dass Ihre neueste Version in der nächsten Simulation verfügbar ist.
Dank AKroell haben wir jetzt mehr Statistiken für jeden Bot ! Die drei neuen Spalten enthalten die maximale Punktzahl für alle Spiele, die durchschnittliche Punktzahl pro Spiel und die durchschnittliche Punktzahl beim Gewinnen für jeden Bot.
Wie in den Kommentaren erwähnt, gab es ein Problem mit der Spiellogik, bei dem Bots mit einem höheren Index innerhalb eines Spiels in einigen Fällen eine Extrarunde erhielten. Dies wurde jetzt behoben, und die unten stehenden Scores spiegeln dies wider.
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|
+-----------------------+----+--------+--------+------+------+-------+------+--------+
Die folgenden Bots (außer Rebel
) dienen dazu, die Regeln zu biegen, und die Macher haben zugestimmt, nicht am offiziellen Turnier teilzunehmen. Ich denke jedoch immer noch, dass ihre Ideen kreativ sind und eine lobende Erwähnung verdienen. Rebel ist auch auf dieser Liste, weil es eine clevere Strategie einsetzt, um Sabotage zu vermeiden, und mit dem Sabotage-Bot, der gerade im Spiel ist, tatsächlich eine bessere Leistung erzielt.
Die Bots NeoBot
und KwisatzHaderach
folgen den Regeln, nutzen aber eine Lücke, indem sie den Zufallsgenerator vorhersagen. Da diese Bots eine Menge Ressourcen zum Simulieren benötigen, habe ich ihre Statistiken aus einer Simulation mit weniger Spielen hinzugefügt. Der Bot HarkonnenBot
gewinnt, indem er alle anderen Bots deaktiviert, was streng gegen die Regeln verstößt.
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|
+----------+-----+