Турнір закінчився!
Турнір зараз закінчився! Остаточне моделювання проводилося протягом ночі, загалом ігор. Переможець - Крістіан Сіверс із його ботом OptFor2X . Крістіану Сіверсу також вдалося закріпити друге місце з Rebel . Вітаємо! Нижче ви можете ознайомитись з офіційним списком високих балів турніру.
Якщо ви все ще хочете пограти в цю гру, вам більше ніж приємно використовувати контролер, розміщений нижче, та використовувати код у ньому для створення власної гри.
Мене запросили пограти в кубики, про які я ніколи не чув. Правила були простими, але я думаю, що це було б ідеально для виклику KotH.
Правила
Початок гри
Штамп обходить стіл, і кожного разу, коли настає ваша черга, ви кидаєте матрицю стільки разів, скільки захочете. Однак вам доведеться кинути його хоча б раз. Ви стежите за сумою всіх кидків за свій раунд. Якщо ви вирішите зупинитись, оцінка за раунд додається до вашої загальної оцінки.
То чому б ти коли-небудь переставав кидати штамп? Тому що якщо ви отримаєте 6, ваш бал за весь раунд стає нульовим, а матриця передається далі. Таким чином, початкова мета - максимально швидко збільшити свій рахунок.
Хто переможець?
Коли перший гравець навколо столу досягає 40 очок і більше, починається останній раунд. Після початку останнього раунду всі, окрім людини, яка ініціювала останній раунд, отримують ще одну чергу.
Правила останнього раунду такі ж, як і для будь-якого іншого раунду. Ви вирішили продовжувати кидати або зупинятись. Однак ви знаєте, що у вас немає шансів на перемогу, якщо ви не отримаєте більш високий бал, ніж ті, що були перед вами в останньому турі. Але якщо ви продовжуєте йти занадто далеко, то ви можете отримати 6.
Однак є ще одне правило, яке слід врахувати. Якщо ваш поточний загальний бал (ваш попередній бал + ваш поточний бал за раунд) становить 40 або більше, а ви набрали 6, ваш загальний бал встановлений на 0. Це означає, що вам потрібно починати все спочатку. Якщо ви потрапили на 6, коли ваш поточний загальний бал становить 40 і більше, гра продовжується як звичайно, за винятком того, що ви зараз на останньому місці. Останній раунд не ініціюється, коли загальний бал буде скинутий. Ви все одно можете виграти раунд, але він стає більш складним.
Переможець - гравець із найвищим балом після закінчення останнього раунду. Якщо два та більше гравців поділяють однаковий рахунок, всі вони будуть зараховані як переможці.
Додане правило полягає в тому, що гра триває максимум 200 раундів. Це запобігає випадкам, коли декілька ботів в основному продовжують кидати, поки не вдаряють 6, щоб залишитися на їх поточному рахунку. Після того, як пройде 199-й раунд, last_round
буде встановлено істинно, і ще один раунд буде розіграний. Якщо гра проходить до 200 раундів, бот (або боти) з найвищим балом є переможцем, навіть якщо у них немає 40 очок і більше.
Резюме
- Кожен раунд ви продовжуєте кидати штамп, поки не вирішите зупинитися або не отримаєте 6
- Ви повинні кинути штамп один раз (якщо ваш перший кидок - 6, ваш раунд негайно закінчився)
- Якщо ви отримаєте 6, ваш поточний бал встановлюється на 0 (не ваш загальний бал)
- Ви додаєте свій поточний бал до загального балу після кожного раунду
- Коли бот закінчує свою чергу, в результаті чого загальний бал становить не менше 40, всі інші отримують останню чергу
- Якщо ваш поточний загальний бал становить а ви отримуєте 6, ваш загальний бал встановлюється на 0, а ваш раунд закінчений
- Останній раунд не спрацьовує, коли відбувається вищезазначене
- Переможець має та людина, яка має найвищий загальний бал після останнього туру
- Якщо переможців буде декілька, усі будуть зараховані як переможці
- Гра триває максимум 200 раундів
Уточнення балів
- Загальний бал: оцінка, яку ви зберегли за попередні раунди
- Поточний рахунок: оцінка за поточний раунд
- Поточний загальний бал: сума двох балів вище
Як ти береш участь
Щоб взяти участь у цьому виклику KotH, вам слід написати клас Python, від якого успадковується Bot
. Ви повинні реалізувати функцію: make_throw(self, scores, last_round)
. Ця функція буде викликана, як тільки ваша черга, а ваш перший кидок не був 6. Щоб продовжувати кидати, ви повинні yield True
. Щоб перестати кидати, слід yield False
. Після кожного кидка update_state
викликається батьківська функція . Таким чином, ви маєте доступ до своїх кидків протягом поточного раунду за допомогою змінної self.current_throws
. Ви також маєте доступ до власного індексу за допомогою self.index
. Таким чином, щоб побачити свій власний загальний бал, який ви використали б scores[self.index]
. Ви також можете отримати доступ до end_score
гри, використовуючи self.end_score
, але можете сміливо припускати, що їй буде 40 для цього виклику.
Вам дозволяється створювати допоміжні функції всередині вашого класу. Ви також можете змінити функції, що існують у Bot
батьківському класі, наприклад, якщо ви хочете додати більше властивостей класу. Вам не дозволяється жодним чином змінювати стан гри, окрім поступаючись True
або False
.
Ви можете шукати натхнення у цій публікації та копіювати будь-який із двох ботів, які я включив сюди. Однак я боюся, що вони не особливо ефективні ...
Дозволення інших мов
І в пісочниці, і в дев'ятнадцятому байті ми мали дискусії щодо дозволу подання матеріалів іншими мовами. Ознайомившись з такими реалізаціями та почувши аргументи з обох сторін, я вирішив обмежити цей виклик лише Python. Це пов'язано з двома факторами: часом, необхідним для підтримки декількох мов, і випадковістю цього виклику, що вимагає великої кількості ітерацій для досягнення стабільності. Я сподіваюся, що ви все одно будете брати участь, і якщо ви хочете дізнатися якийсь Python для цього виклику, я спробую бути доступним у чаті якомога частіше.
З будь-яких питань, які можуть виникнути, ви можете написати в чаті для цього виклику . Побачимось!
Правила
- Диверсії дозволяються та заохочуються. Тобто, диверсія проти інших гравців
- Будь-яка спроба повозитися з контролером, часом виконання чи іншими поданнями буде дискваліфікована. Усі подання повинні працювати лише з наданими входами та сховищами.
- Будь-який бот, який використовує більше 500 МБ пам'яті для прийняття свого рішення, буде дискваліфікований (якщо вам потрібно стільки пам'яті, вам слід переосмислити свій вибір)
- Бот не повинен реалізовувати ту саму стратегію, що й існуюча, навмисно чи випадково.
- Ви можете оновлювати свого бота під час виклику. Однак ви також можете опублікувати іншого бота, якщо ваш підхід відрізняється.
Приклад
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
Цей бот буде продовжувати, поки у нього не буде бал щонайменше 10 за раунд, або він закине 6. Зверніть увагу, що вам не потрібна логіка для кидка 6. Також врахуйте, що якщо ваш перший кидок - це 6, make_throw
це ніколи не дзвонили, оскільки ваш раунд негайно закінчився
Для тих, хто не знайомий з Python (і новим у yield
концепції), але хочуть зробити це, yield
ключове слово в чомусь схоже на повернення, але іншим чином відрізняється. Про концепцію ви можете прочитати тут . В основному, як тільки ви yield
, ваша функція припиниться, і значення, яке ви yield
редагували, буде відправлено назад до контролера. Там контролер обробляє свою логіку, поки не настане час, коли ваш бот прийме інше рішення. Тоді контролер надсилає вам кидання кістки, і ваша make_throw
функція буде продовжувати виконувати прямо там, де якщо зупинено раніше, в основному на лінії після попереднього yield
твердження.
Таким чином, ігровий контролер може оновлювати стан, не вимагаючи окремого виклику функції бота для кожного кидання кістки.
Специфікація
Ви можете використовувати будь-яку бібліотеку Python, доступну в pip
. Щоб переконатися, що мені вдасться отримати хороший середній показник, у вас є обмеження в 100 мілісекунд часу на раунд. Я був би дуже радий, якби ваш сценарій був набагато швидшим за це, щоб я міг пробігти більше раундів.
Оцінка
Щоб знайти переможця, я візьму всіх ботів і проведу їх у випадкових групах з 8. Якщо подано менше 8 класів, я запускатиму їх у випадкових групах по 4, щоб у кожному раунді не було всіх ботів. Я буду проводити симуляції близько 8 годин, а переможцем стане бот з найвищим відсотком виграшу. Я розпочну фінальні симуляції на початку 2019 року, даючи вам все Різдво, щоб кодувати ваших ботів! Попередня кінцева дата - 4 січня, але якщо це занадто мало часу, я можу змінити її на більш пізню дату.
До цього часу я спробую зробити щоденне моделювання, використовуючи 30-60 хвилин процесорного часу та оновлюючи табло. Це не буде офіційним рахунком, але він послужить керівництвом, щоб побачити, які боти працюють найкраще. Однак, з Різдвом, я сподіваюся, ви зможете зрозуміти, що я не буду доступний постійно. Я зроблю все можливе, щоб запускати симуляції та відповідати на будь-які питання, пов’язані із завданням.
Перевірте це самостійно
Якщо ви хочете запустити свої власні симуляції, ось повний код контролера, що виконує моделювання, включаючи два приклади ботів.
Контролер
Ось оновлений контролер для цього виклику. Він підтримує виходи ANSI, багатопотокові та збирає додаткові статистичні дані завдяки AKroell ! Коли я вношу зміни до контролера, я оновлю публікацію, коли документація буде завершена.
Завдяки BMO , контролер тепер може завантажувати всі боти з цієї посади, використовуючи -d
прапор. Інша функціональність не змінюється в цій версії. Це має забезпечити якнайшвидше моделювання всіх останніх змін!
#!/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)
Якщо ви хочете отримати доступ до оригінального контролера для цього виклику, він доступний в історії редагування. Новий контролер має точно таку ж логіку для запуску гри, різниця лише в продуктивності, збір статистики і краща друк.
Боти
На моїй машині боти зберігаються у файлі forty_game_bots.py
. Якщо ви використовуєте будь-яке інше ім'я для файлу, ви повинні оновити import
оператор у верхній частині контролера.
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
Запуск моделювання
Для запуску моделювання збережіть обидва фрагменти коду, розміщені вище, у два окремі файли. Я врятував їх як forty_game_controller.py
і forty_game_bots.py
. Тоді ви просто використовуєте python forty_game_controller.py
або python3 forty_game_controller.py
залежно від конфігурації Python. Дотримуйтесь інструкцій звідти, якщо ви хочете далі налаштувати своє моделювання, або спробуйте попрацювати з кодом, якщо хочете.
Статистика ігор
Якщо ви робите бота, який націлений на певний рахунок, не враховуючи інших ботів, це відсоткові виграшні бали:
+----------+-----+
|Percentile|Score|
+----------+-----+
| 50.00| 44|
| 75.00| 48|
| 90.00| 51|
| 95.00| 54|
| 99.00| 58|
| 99.90| 67|
| 99.99| 126|
+----------+-----+
Високі бали
Оскільки більше відповідей розміщено, я спробую постійно оновлювати цей список. Вміст списку завжди буде від останнього моделювання. Боти ThrowTwiceBot
і GoToTenBot
є ботами з наведеного вище коду і використовуються як посилання. Я зробив моделювання з 10 ^ 8 ігор, на що пішло близько 1 години. Потім я побачив, що гра досягає стабільності порівняно з моїми пробіжками з 10 ^ 7 ігор. Однак, коли люди все ще публікують ботів, я більше не буду робити симуляції, поки частота відповідей не знизиться.
Я намагаюся додати всі нові боти і внести будь-які зміни, які ви внесли до існуючих ботів. Якщо вам здається, що я пропустив вашого бота або будь-які нові ваші зміни, напишіть у чаті, і я переконайтеся, що ви отримаєте останню версію в наступному моделюванні.
Тепер у нас є більше статистики для кожного бота завдяки AKroell ! Три нові колонки містять максимальний бал у всіх іграх, середній бал за гру та середній бал при виграші за кожного бота.
Як зазначалося в коментарях, виникла проблема з логікою гри, яка змусила ботів, які мали більш високий індекс у грі, отримати додатковий раунд у деяких випадках. Це було виправлено зараз, і оцінки нижче відображають це.
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|
+-----------------------+----+--------+--------+------+------+-------+------+--------+
Наступні боти (крім Rebel
) зроблені для того, щоб виконувати правила, і творці погодилися не брати участі в офіційному турнірі. Однак я все ще думаю, що їхні ідеї є творчими, і вони заслуговують на почесну згадку. Rebel також є у цьому списку, оскільки він використовує розумну стратегію, щоб уникнути диверсій, і насправді краще працює з ботом-саботажем у грі.
Боти NeoBot
і KwisatzHaderach
справді дотримуються правил, але використовують лазівку, передбачуючи випадковий генератор. Оскільки ці боти потребують багато ресурсів для імітації, я додав її статистику із симуляції з меншою кількістю ігор. Бот HarkonnenBot
домагається перемоги, відключаючи всіх інших ботів, що суворо суперечить правилам.
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|
+----------+-----+