Turniej się skończył!
Turniej się zakończył! Ostateczna symulacja została przeprowadzona w nocy, łącznie gier. Zwycięzcą został Christian Sievers ze swoim botem OptFor2X . Christian Sievers zdołał także zabezpieczyć drugie miejsce Rebelią . Gratulacje! Poniżej znajduje się oficjalna lista najlepszych wyników turnieju.
Jeśli nadal chcesz zagrać w tę grę, z przyjemnością skorzystaj z kontrolera zamieszczonego poniżej i użyj kodu w nim, aby stworzyć własną grę.
Zostałem zaproszony do gry w kości, o której nigdy nie słyszałem. Zasady były proste, ale myślę, że byłoby idealne na wyzwanie KotH.
Zasady
Początek gry
Kostka porusza się wokół stołu i za każdym razem, gdy jest twoja kolej, możesz rzucić kostką tyle razy, ile chcesz. Musisz jednak rzucić to przynajmniej raz. Śledzisz sumę wszystkich rzutów w swojej rundzie. Jeśli zdecydujesz się zatrzymać, wynik za rundę zostanie dodany do całkowitego wyniku.
Dlaczego więc miałbyś przestać rzucać kostką? Ponieważ jeśli zdobędziesz 6, twój wynik za całą rundę wyniesie zero, a kość zostanie przekazana. Zatem początkowym celem jest jak najszybsze zwiększenie wyniku.
Kto jest zwycięzcą?
Gdy pierwszy gracz przy stole osiągnie 40 lub więcej punktów, rozpoczyna się ostatnia runda. Po rozpoczęciu ostatniej rundy wszyscy oprócz osoby, która zainicjowała ostatnią rundę, dostaje jeszcze jedną turę.
Zasady dla ostatniej rundy są takie same jak dla każdej innej rundy. Wybierasz rzucanie lub przestaniesz. Wiesz jednak, że nie masz szans na wygraną, jeśli nie uzyskasz wyższego wyniku niż przed ostatnią rundą. Ale jeśli posuniesz się za daleko, możesz otrzymać 6.
Należy jednak wziąć pod uwagę jeszcze jedną zasadę. Jeśli bieżący całkowity wynik (poprzedni wynik + aktualny wynik w rundzie) wynosi 40 lub więcej i trafisz 6, łączny wynik jest ustawiony na 0. Oznacza to, że musisz zacząć wszystko od nowa. Jeśli trafisz 6, gdy obecny całkowity wynik wynosi 40 lub więcej, gra będzie kontynuowana normalnie, z tym wyjątkiem, że jesteś teraz na ostatnim miejscu. Ostatnia runda nie jest uruchamiana po zresetowaniu całkowitego wyniku. Nadal możesz wygrać rundę, ale staje się ona trudniejsza.
Zwycięzcą zostaje gracz z najwyższym wynikiem po zakończeniu ostatniej rundy. Jeśli dwóch lub więcej graczy podzieli ten sam wynik, wszyscy będą liczeni jako zwycięzcy.
Dodatkową zasadą jest to, że gra trwa maksymalnie 200 rund. Ma to na celu zapobieganie przypadkom, w których wiele botów w zasadzie rzuca, dopóki nie osiągnie 6, aby utrzymać swój obecny wynik. Po przejściu 199. rundy zostaje last_round
ustawiona na wartość true i rozpoczyna się jeszcze jedna runda. Jeśli gra przejdzie do 200 rund, zwycięzcą jest bot (lub boty) z najwyższym wynikiem, nawet jeśli nie mają 40 punktów lub więcej.
Podsumować
- W każdej rundzie rzucasz kością, dopóki nie zatrzymasz się lub nie otrzymasz 6
- Musisz rzucić kostką raz (jeśli twój pierwszy rzut to 6, twoja runda natychmiast się kończy)
- Jeśli otrzymasz 6, twój aktualny wynik jest ustawiony na 0 (nie twój całkowity wynik)
- Po każdej rundzie dodajesz swój aktualny wynik do całkowitego wyniku
- Kiedy bot kończy swoją turę, co daje łączny wynik co najmniej 40, wszyscy inni otrzymują ostatnią turę
- Jeśli twój obecny całkowity wynik to a otrzymasz 6, twój całkowity wynik jest ustawiony na 0 i Twoja runda się kończy
- Ostatnia runda nie jest uruchamiana, gdy wystąpi powyższe
- Zwycięzcą zostaje osoba z najwyższym łącznym wynikiem po ostatniej rundzie
- W przypadku wielu zwycięzców, wszyscy będą liczeni jako zwycięzcy
- Gra trwa maksymalnie 200 rund
Wyjaśnienie wyników
- Łączny wynik: wynik zapisany z poprzednich rund
- Aktualny wynik: wynik dla bieżącej rundy
- Aktualny łączny wynik: suma dwóch powyższych wyników
Jak uczestniczysz
Aby wziąć udział w tym wyzwaniu KotH, powinieneś napisać klasę Python, która dziedziczy po Bot
. Należy wdrożyć funkcję: make_throw(self, scores, last_round)
. Ta funkcja zostanie wywołana, gdy nadejdzie Twoja kolej, a twój pierwszy rzut nie był 6. Aby kontynuować rzucanie, powinieneś yield True
. Aby przestać rzucać, powinieneś yield False
. Po każdym rzucie update_state
wywoływana jest funkcja rodzica . Dzięki temu masz dostęp do swoich rzutów w bieżącej rundzie za pomocą zmiennej self.current_throws
. Masz również dostęp do własnego indeksu za pomocą self.index
. Zatem, aby zobaczyć swój własny wynik, którego byś użył scores[self.index]
. Możesz również uzyskać dostęp end_score
do gry za pomocą self.end_score
, ale możesz bezpiecznie założyć, że będzie to 40 za to wyzwanie.
Możesz tworzyć funkcje pomocnicze w swojej klasie. Możesz również zastąpić funkcje istniejące w Bot
klasie nadrzędnej, np. Jeśli chcesz dodać więcej właściwości klasy. Nie możesz modyfikować stanu gry w jakikolwiek sposób, z wyjątkiem ustępowania True
lub False
.
Możesz szukać inspiracji w tym poście i skopiować dowolny z dwóch botów, które tu zawarłem. Obawiam się jednak, że nie są szczególnie skuteczne ...
Zezwalanie na inne języki
Zarówno w piaskownicy, jak i w The Nineteenth Byte, rozmawialiśmy o dopuszczaniu zgłoszeń w innych językach. Po przeczytaniu o takich implementacjach i wysłuchaniu argumentów z obu stron postanowiłem ograniczyć to wyzwanie tylko do Pythona. Wynika to z dwóch czynników: czasu wymaganego do obsługi wielu języków oraz losowości tego wyzwania wymagającego dużej liczby iteracji w celu osiągnięcia stabilności. Mam nadzieję, że nadal będziesz brać udział, a jeśli chcesz nauczyć się Pythona do tego wyzwania, postaram się być dostępny na czacie tak często, jak to możliwe.
W przypadku jakichkolwiek pytań możesz napisać w pokoju czatu dotyczącym tego wyzwania . Do zobaczenia tam!
Zasady
- Sabotaż jest dozwolony i zachęcany. To znaczy sabotaż przeciwko innym graczom
- Wszelkie próby majsterkowania przy kontrolerze, czasie wykonywania lub innych zgłoszeniach zostaną zdyskwalifikowane. Wszystkie zgłoszenia powinny działać tylko z danymi wejściowymi i pamięcią.
- Każdy bot, który wykorzystuje więcej niż 500 MB pamięci do podjęcia decyzji, zostanie zdyskwalifikowany (jeśli potrzebujesz takiej ilości pamięci, powinieneś przemyśleć swoje wybory)
- Bot nie może wdrożyć dokładnie tej samej strategii, co istniejąca, celowo lub przypadkowo.
- W trakcie wyzwania możesz aktualizować swojego bota. Możesz jednak również opublikować innego bota, jeśli Twoje podejście jest inne.
Przykład
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
Ten bot będzie działał, dopóki nie uzyska wyniku co najmniej 10 w rundzie lub nie wyrzuci 6. Zauważ, że nie potrzebujesz żadnej logiki, aby poradzić sobie z rzucaniem 6. Pamiętaj również, że jeśli twój pierwszy rzut to 6, make_throw
to nigdy nie sprawdzany, ponieważ Twoja runda natychmiast się kończy.
Dla tych, którzy są nowi w Pythonie (i nowi w yield
koncepcji), ale chcą spróbować, yield
słowo kluczowe jest pod pewnymi względami podobne do zwrotu, ale różni się pod innymi względami. O tej koncepcji możesz przeczytać tutaj . Zasadniczo, gdy ty yield
, twoja funkcja zatrzyma się, a edytowana wartość yield
zostanie odesłana z powrotem do kontrolera. Tam kontroler obsługuje logikę, aż nadejdzie czas, aby bot podjął kolejną decyzję. Następnie kontroler wyśle ci rzut kostką, a twoja make_throw
funkcja będzie kontynuowała wykonywanie dokładnie tam, gdzie została zatrzymana wcześniej, zasadniczo na linii po poprzedniej yield
instrukcji.
W ten sposób kontroler gier może aktualizować stan bez konieczności oddzielnego wywołania funkcji bota dla każdego rzutu kostką.
Specyfikacja
Możesz użyć dowolnej biblioteki Python dostępnej w pip
. Aby upewnić się, że będę w stanie uzyskać dobrą średnią, masz limit 100 milisekund na rundę. Byłbym bardzo szczęśliwy, gdyby twój skrypt był o wiele szybszy, abym mógł uruchomić więcej rund.
Ocena
Aby znaleźć zwycięzcę, wezmę wszystkie boty i uruchomię je w losowych grupach po 8 osób. Jeśli zgłoszono mniej niż 8 klas, poprowadzę je w losowych grupach po 4, aby uniknąć zawsze posiadania wszystkich botów w każdej rundzie. Będę przeprowadzał symulacje przez około 8 godzin, a zwycięzcą zostanie bot z najwyższym procentem wygranych. Rozpocznę końcowe symulacje na początku 2019 roku, dając wam wszystkie Święta Bożego Narodzenia do kodowania botów! Wstępna data końcowa to 4 stycznia, ale jeśli to za mało czasu, mogę zmienić ją na późniejszą.
Do tego czasu spróbuję wykonać codzienną symulację z wykorzystaniem 30–60 minut czasu procesora i zaktualizować tablicę wyników. To nie będzie oficjalny wynik, ale posłuży jako przewodnik do sprawdzenia, które boty działają najlepiej. Jednak zbliża się Boże Narodzenie, mam nadzieję, że zrozumiesz, że nie będę przez cały czas dostępny. Zrobię co w mojej mocy, aby przeprowadzić symulacje i odpowiedzieć na wszelkie pytania związane z wyzwaniem.
Sprawdź to sam
Jeśli chcesz uruchomić własne symulacje, oto pełny kod kontrolera uruchamiającego symulację, w tym dwa przykładowe boty.
Kontroler
Oto zaktualizowany kontroler tego wyzwania. Obsługuje wyjścia ANSI, wielowątkowość i zbiera dodatkowe statystyki dzięki AKroell ! Kiedy wprowadzę zmiany w kontrolerze, zaktualizuję post po zakończeniu dokumentacji.
Dzięki BMO kontroler może teraz pobierać wszystkie boty z tego postu za pomocą -d
flagi. Inne funkcje pozostają niezmienione w tej wersji. To powinno zapewnić, że wszystkie twoje najnowsze zmiany zostaną jak najszybciej zasymulowane!
#!/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)
Jeśli chcesz uzyskać dostęp do oryginalnego kontrolera dla tego wyzwania, jest on dostępny w historii edycji. Nowy kontroler ma tę samą logikę do uruchamiania gry, jedyną różnicą jest wydajność, zbieranie statystyk i ładniejsze drukowanie.
Boty
Na moim komputerze boty są przechowywane w pliku forty_game_bots.py
. Jeśli używasz innej nazwy pliku, musisz zaktualizować import
instrukcję na górze kontrolera.
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
Uruchamianie symulacji
Aby uruchomić symulację, zapisz oba fragmenty kodu opublikowane powyżej w dwóch osobnych plikach. Uratowałem je jako forty_game_controller.py
i forty_game_bots.py
. Następnie wystarczy użyć python forty_game_controller.py
lub w python3 forty_game_controller.py
zależności od konfiguracji Pythona. Postępuj zgodnie z instrukcjami, jeśli chcesz dalej konfigurować symulację, lub jeśli chcesz, majstruj przy kodzie.
Statystyki gry
Jeśli tworzysz bota, który dąży do określonego wyniku bez uwzględnienia innych botów, są to zwycięskie percentyle:
+----------+-----+
|Percentile|Score|
+----------+-----+
| 50.00| 44|
| 75.00| 48|
| 90.00| 51|
| 95.00| 54|
| 99.00| 58|
| 99.90| 67|
| 99.99| 126|
+----------+-----+
Wysokie wyniki
W miarę publikowania kolejnych odpowiedzi postaram się aktualizować tę listę. Zawartość listy zawsze będzie pochodzić z najnowszej symulacji. Boty ThrowTwiceBot
i GoToTenBot
są botami z powyższego kodu i są używane jako odniesienie. Przeprowadziłem symulację z 10 ^ 8 grami, co zajęło około 1 godziny. Potem zobaczyłem, że gra osiągnęła stabilność w porównaniu do moich biegów z 10 ^ 7 grami. Ponieważ jednak ludzie nadal publikują boty, nie będę wykonywać żadnych symulacji, dopóki częstotliwość odpowiedzi nie spadnie.
Próbuję dodać wszystkie nowe boty i wszelkie zmiany, które wprowadziłeś w istniejących botach. Jeśli wygląda na to, że przegapiłem twojego bota lub jakieś nowe zmiany, które masz, napisz na czacie, a upewnię się, że w następnej symulacji będę mieć twoją najnowszą wersję.
Mamy teraz więcej statystyk dla każdego bota dzięki AKroell ! Trzy nowe kolumny zawierają maksymalny wynik we wszystkich grach, średni wynik na grę i średni wynik przy wygrywaniu dla każdego bota.
Jak wskazano w komentarzach, wystąpił problem z logiką gry, w wyniku którego boty o wyższym indeksie w grze otrzymywały dodatkową rundę w niektórych przypadkach. Zostało to już naprawione i odzwierciedlają to poniższe wyniki.
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|
+-----------------------+----+--------+--------+------+------+-------+------+--------+
Następujące boty (oprócz Rebel
) są tworzone w celu naginania zasad, a twórcy zgodzili się nie brać udziału w oficjalnym turnieju. Jednak nadal uważam, że ich pomysły są kreatywne i zasługują na wyróżnienie. Rebel jest również na tej liście, ponieważ używa sprytnej strategii, aby uniknąć sabotażu, i faktycznie działa lepiej z botem sabotażowym w grze.
Boty NeoBot
i KwisatzHaderach
postępują zgodnie z zasadami, ale wykorzystują lukę, przewidując losowy generator. Ponieważ boty te wymagają wielu zasobów do symulacji, dodałem statystyki z symulacji z mniejszą liczbą gier. Bot HarkonnenBot
osiąga zwycięstwo poprzez wyłączenie wszystkich innych botów, co jest niezgodne z zasadami.
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|
+----------+-----+