Torneio terminado!
O torneio acabou agora! A simulação final foi realizada durante a noite, totalizando jogos. O vencedor é Christian Sievers com seu bot OptFor2X . Christian Sievers também conseguiu garantir o segundo lugar com Rebel . Parabéns! Abaixo você pode ver a lista oficial de recordes do torneio.
Se você ainda deseja jogar, é bem-vindo ao usar o controlador postado abaixo e ao usar o código nele para criar seu próprio jogo.
Fui convidado a jogar um jogo de dados que nunca tinha ouvido falar. As regras eram simples, mas acho que seriam perfeitas para um desafio KotH.
As regras
O começo do jogo
O dado gira em torno da mesa e, toda vez que é a sua vez, você joga o dado quantas vezes quiser. No entanto, você deve jogá-lo pelo menos uma vez. Você acompanha a soma de todos os lançamentos da sua rodada. Se você optar por parar, a pontuação da rodada será adicionada à sua pontuação total.
Então, por que você pararia de jogar o dado? Porque se você obtiver 6, sua pontuação na rodada inteira será zero e o dado será transmitido. Assim, o objetivo inicial é aumentar sua pontuação o mais rápido possível.
Quem é o vencedor?
Quando o primeiro jogador em volta da mesa atinge 40 pontos ou mais, a última rodada começa. Uma vez iniciada a última rodada, todos, exceto a pessoa que iniciou a última rodada, recebem mais um turno.
As regras para a última rodada são as mesmas de qualquer outra rodada. Você escolhe continuar jogando ou parar. No entanto, você sabe que não tem chance de ganhar se não obtiver uma pontuação maior que a anterior na última rodada. Mas se você continuar indo longe demais, poderá receber um 6.
No entanto, há mais uma regra a ser levada em consideração. Se sua pontuação total atual (sua pontuação anterior + sua pontuação atual da rodada) for 40 ou mais e você acertar um 6, sua pontuação total será 0. Isso significa que você deve começar tudo de novo. Se você acertar um 6 quando sua pontuação total atual for 40 ou mais, o jogo continuará normal, exceto que você está no último lugar. A última rodada não é acionada quando sua pontuação total é redefinida. Você ainda pode vencer a rodada, mas ela se torna mais desafiadora.
O vencedor é o jogador com a maior pontuação assim que a última rodada terminar. Se dois ou mais jogadores compartilharem a mesma pontuação, todos serão contados como vencedores.
Uma regra adicional é que o jogo continue por no máximo 200 rodadas. Isso evita os casos em que vários bots basicamente continuam jogando até atingir 6 para permanecer na pontuação atual. Depois que a 199ª rodada é aprovada, last_round
é definido como verdadeiro e mais uma rodada é jogada. Se o jogo for para 200 rodadas, o bot (ou bots) com a maior pontuação será o vencedor, mesmo que eles não tenham 40 pontos ou mais.
Recapitular
- A cada rodada, você continua jogando o dado até optar por parar ou obter um 6
- Você deve jogar o dado uma vez (se o seu primeiro lançamento for um 6, sua rodada termina imediatamente)
- Se você obtiver um 6, sua pontuação atual será definida como 0 (não a pontuação total)
- Você adiciona sua pontuação atual à sua pontuação total após cada rodada
- Quando um bot termina seu turno, resultando em uma pontuação total de pelo menos 40, todo mundo recebe o último turno
- Se sua pontuação total atual é e você obtém um 6, sua pontuação total é definida como 0 e sua rodada termina.
- A última rodada não é acionada quando ocorrer o acima
- A pessoa com a maior pontuação total após a última rodada é o vencedor
- Caso haja vários vencedores, todos serão contados como vencedores
- O jogo dura no máximo 200 rodadas
Esclarecimento das pontuações
- Pontuação total: a pontuação que você salvou das rodadas anteriores
- Pontuação atual: a pontuação da rodada atual
- Pontuação total atual: a soma das duas pontuações acima
Como você participa
Para participar deste desafio do KotH, você deve escrever uma classe Python que herda Bot
. Você deve implementar a função: make_throw(self, scores, last_round)
. Essa função será chamada assim que for a sua vez, e seu primeiro lançamento não for 6. Para continuar jogando, você deve yield True
. Para parar de jogar, você deveria yield False
. Após cada lançamento, a função pai update_state
é chamada. Assim, você tem acesso a sua lança para a rodada atual usando a variável self.current_throws
. Você também tem acesso ao seu próprio índice usando self.index
. Assim, para ver sua própria pontuação total, você usaria scores[self.index]
. Você também pode acessar o end_score
para o jogo usando self.end_score
, mas pode assumir com segurança que serão 40 para esse desafio.
Você tem permissão para criar funções auxiliares dentro da sua classe. Você também pode substituir as funções existentes na Bot
classe pai, por exemplo, se desejar adicionar mais propriedades de classe. Você não tem permissão para modificar o estado do jogo de qualquer forma, exceto cedendo True
ou False
.
Você pode buscar inspiração nesta postagem e copiar qualquer um dos dois bots que incluí aqui. No entanto, receio que não sejam particularmente eficazes ...
Ao permitir outros idiomas
Tanto na sandbox quanto no The XIX XIX Byte, tivemos discussões sobre como permitir envios em outros idiomas. Depois de ler sobre essas implementações e ouvir argumentos de ambos os lados, decidi restringir esse desafio apenas ao Python. Isso se deve a dois fatores: o tempo necessário para suportar vários idiomas e a aleatoriedade desse desafio, exigindo um número alto de iterações para alcançar a estabilidade. Espero que você ainda participe e, se quiser aprender algum Python para esse desafio, tentarei estar disponível no bate-papo o mais rápido possível.
Para qualquer dúvida que possa ter, escreva na sala de bate-papo para este desafio . Vejo você lá!
Regras
- A sabotagem é permitida e incentivada. Ou seja, sabotar contra outros jogadores
- Qualquer tentativa de mexer com o controlador, tempo de execução ou outros envios será desqualificada. Todos os envios devem funcionar apenas com as entradas e armazenamento fornecidos.
- Qualquer bot que use mais de 500 MB de memória para tomar sua decisão será desqualificado (se você precisar de tanta memória, deve repensar suas escolhas)
- Um bot não deve implementar exatamente a mesma estratégia que uma existente, intencional ou acidentalmente.
- Você tem permissão para atualizar seu bot durante o período do desafio. No entanto, você também pode postar outro bot se sua abordagem for diferente.
Exemplo
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
Esse bot continuará até ter uma pontuação de pelo menos 10 na rodada ou lançar um 6. Observe que você não precisa de lógica para lidar com o arremesso 6. Observe também que, se seu primeiro arremesso for 6, make_throw
é nunca ligou, já que sua rodada termina imediatamente.
Para quem é novo no Python (e novo no yield
conceito), mas quer experimentar, a yield
palavra-chave é semelhante a um retorno em alguns aspectos, mas diferente em outros aspectos. Você pode ler sobre o conceito aqui . Basicamente, uma vez que você yield
, sua função será interrompida, e o valor yield
editado será enviado de volta ao controlador. Lá, o controlador lida com sua lógica até a hora de seu bot tomar outra decisão. Em seguida, o controlador envia o lançamento dos dados e sua make_throw
função continuará sendo executada exatamente onde parou antes, basicamente na linha após a yield
instrução anterior .
Dessa forma, o controlador do jogo pode atualizar o estado sem exigir uma chamada de função bot separada para cada lançamento de dados.
Especificação
Você pode usar qualquer biblioteca Python disponível em pip
. Para garantir que eu seja capaz de obter uma boa média, você tem um limite de tempo de 100 milissegundos por rodada. Eu ficaria muito feliz se o seu script fosse muito mais rápido que isso, para que eu possa rodar mais rodadas.
Avaliação
Para encontrar o vencedor, pegarei todos os bots e os executarei em grupos aleatórios de 8. Se houver menos de 8 classes enviadas, executarei em grupos aleatórios de 4 para evitar sempre ter todos os bots em cada rodada. Vou executar simulações por cerca de 8 horas, e o vencedor será o bot com a maior porcentagem de vitórias. Vou iniciar as simulações finais no início de 2019, dando todo o Natal para codificar seus bots! A data final preliminar é 4 de janeiro, mas, se houver muito pouco tempo, posso alterá-la para uma data posterior.
Até lá, tentarei fazer uma simulação diária usando 30 a 60 minutos de tempo da CPU e atualizando o placar. Essa não será a pontuação oficial, mas servirá como um guia para ver quais robôs têm o melhor desempenho. No entanto, com o Natal chegando, espero que você entenda que eu não estarei disponível o tempo todo. Farei o meu melhor para executar simulações e responder a quaisquer perguntas relacionadas ao desafio.
Teste você mesmo
Se você deseja executar suas próprias simulações, aqui está o código completo do controlador que está executando a simulação, incluindo dois bots de exemplo.
Controlador
Aqui está o controlador atualizado para este desafio. Ele suporta saídas ANSI, multi-threading e coleta estatísticas adicionais graças ao AKroell ! Quando eu fizer alterações no controlador, atualizarei a postagem assim que a documentação estiver concluída.
Graças ao BMO , o controlador agora pode baixar todos os bots desta postagem usando a -d
bandeira. Outra funcionalidade é inalterada nesta versão. Isso deve garantir que todas as suas alterações mais recentes sejam simuladas o mais rápido possível!
#!/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)
Se você deseja acessar o controlador original para esse desafio, ele está disponível no histórico de edições. O novo controlador possui exatamente a mesma lógica para executar o jogo, a única diferença é desempenho, coleta de estatísticas e impressão mais bonita.
Bots
Na minha máquina, os bots são mantidos no arquivo forty_game_bots.py
. Se você usar outro nome para o arquivo, atualize a import
instrução na parte superior do controlador.
import sys, inspect
import random
import numpy as np
# Returns a list of all bot classes which inherit from the Bot class
def get_all_bots():
return Bot.__subclasses__()
# The parent class for all bots
class Bot:
def __init__(self, index, end_score):
self.index = index
self.end_score = end_score
def update_state(self, current_throws):
self.current_throws = current_throws
def make_throw(self, scores, last_round):
yield False
class ThrowTwiceBot(Bot):
def make_throw(self, scores, last_round):
yield True
yield False
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
Executando a simulação
Para executar uma simulação, salve os dois trechos de código publicados acima em dois arquivos separados. Eu os salvei como forty_game_controller.py
e forty_game_bots.py
. Então você simplesmente usa python forty_game_controller.py
ou python3 forty_game_controller.py
depende da sua configuração do Python. Siga as instruções de lá se desejar configurar ainda mais sua simulação ou tente mexer no código, se desejar.
Estatísticas do jogo
Se você está criando um bot que visa uma determinada pontuação sem levar em consideração outros bots, estes são os percentis de pontuação vencedora:
+----------+-----+
|Percentile|Score|
+----------+-----+
| 50.00| 44|
| 75.00| 48|
| 90.00| 51|
| 95.00| 54|
| 99.00| 58|
| 99.90| 67|
| 99.99| 126|
+----------+-----+
Notas altas
À medida que mais respostas forem postadas, tentarei manter essa lista atualizada. O conteúdo da lista sempre será da última simulação. Os bots ThrowTwiceBot
e GoToTenBot
são os bots do código acima e são usados como referência. Fiz uma simulação com 10 ^ 8 jogos, o que levou cerca de 1 hora. Então eu vi que o jogo alcançou estabilidade em comparação com minhas corridas com 10 ^ 7 jogos. No entanto, com as pessoas ainda postando bots, não farei mais simulações até que a frequência das respostas diminua.
Tento adicionar todos os novos bots e quaisquer alterações feitas nos bots existentes. Se parece que eu perdi o seu bot ou qualquer alteração nova que você tenha, escreva no chat e eu me certificarei de ter a sua versão mais recente na próxima simulação.
Agora temos mais estatísticas para cada bot, graças ao AKroell ! As três novas colunas contêm a pontuação máxima em todos os jogos, a pontuação média por jogo e a pontuação média ao ganhar para cada bot.
Como apontado nos comentários, houve um problema com a lógica do jogo que fez bots com um índice mais alto em um jogo obter uma rodada extra em alguns casos. Isso foi corrigido agora, e as pontuações abaixo refletem isso.
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|
+-----------------------+----+--------+--------+------+------+-------+------+--------+
Os seguintes bots (exceto Rebel
) são feitos para violar as regras, e os criadores concordaram em não participar do torneio oficial. No entanto, ainda acho que suas idéias são criativas e merecem uma menção honrosa. Rebelde também está nesta lista porque usa uma estratégia inteligente para evitar sabotagem e, na verdade, tem um desempenho melhor com o bot de sabotagem em jogo.
Os bots NeoBot
e KwisatzHaderach
faz seguir as regras, mas usa uma brecha prevendo o gerador aleatório. Como esses robôs precisam de muitos recursos para simular, adicionei suas estatísticas a partir de uma simulação com menos jogos. O bot HarkonnenBot
alcança a vitória desativando todos os outros bots, o que é estritamente contra as regras.
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|
+----------+-----+