토너먼트 오버!
토너먼트가 끝났습니다! 최종 시뮬레이션은 밤 동안 총 게임으로 진행되었습니다. 우승자는 봇 OptFor2X 와 함께한 크리스천 시버 입니다 . Christian Sievers는 Rebel을 통해 2 위를 차지했습니다 . 축하합니다! 아래는 토너먼트의 공식 최고 점수 목록입니다.
여전히 게임을 즐기고 싶다면 아래에 게시 된 컨트롤러를 사용하고 자신의 게임을 만드는 데 코드를 사용하는 것이 좋습니다.
나는 들어 본 적이없는 주사위 게임을하도록 초대 받았습니다. 규칙은 간단했지만 KotH 도전에 완벽하다고 생각합니다.
규칙
게임의 시작
주사위는 테이블 주위를 돌아 다니며, 차례가 될 때마다 원하는만큼 주사위를 던져야합니다. 그러나 적어도 한 번은 던져야합니다. 당신은 당신의 라운드에 대한 모든 던져의 합계를 추적합니다. 중지하기로 선택하면 해당 라운드의 점수가 총 점수에 추가됩니다.
그렇다면 왜 주사위 던지기를 그만두겠습니까? 6을 얻으면 전체 라운드의 점수가 0이되고 주사위가 통과합니다. 따라서 초기 목표는 가능한 빨리 점수를 높이는 것입니다.
승자는 누구입니까?
테이블 주위의 첫 번째 플레이어가 40 점 이상에 도달하면 마지막 라운드가 시작됩니다. 마지막 라운드가 시작되면 마지막 라운드를 시작한 사람을 제외한 모든 사람이 한 번 더 회전합니다.
마지막 라운드의 규칙은 다른 라운드와 동일합니다. 계속 던지거나 멈추도록 선택합니다. 그러나, 당신은 당신이 마지막 라운드에서 이전보다 높은 점수를 얻지 못하면 이길 확률이 없다는 것을 알고 있습니다. 그러나 계속 너무 멀리 가면 6이 나올 수 있습니다.
그러나 고려해야 할 규칙이 하나 더 있습니다. 현재 총 점수 (이전 점수 + 라운드의 현재 점수)가 40 이상이고 6에 도달하면 총 점수 가 0으로 설정됩니다. 즉 , 전체 를 다시 시작해야합니다. 현재 총 점수가 40 이상일 때 6을 기록하면 게임은 현재 마지막 위치에 있다는 점을 제외하고 정상적으로 계속 진행됩니다. 총 점수가 재설정 될 때 마지막 라운드는 트리거되지 않습니다. 당신은 여전히 라운드에서 이길 수는 있지만 더 어려워집니다.
마지막 라운드가 끝나면 승자가 가장 높은 점수를받습니다. 둘 이상의 플레이어가 같은 점수를 공유하면 모두 승자로 계산됩니다.
추가 된 규칙은 게임이 최대 200 라운드 동안 계속된다는 것입니다. 이것은 여러 봇이 기본적으로 6을 기록 할 때까지 현재 점수를 유지하는 경우를 방지하기위한 것입니다. 199 번째 라운드가 끝나면 last_round
true로 설정되고 한 번 더 라운드가 재생됩니다. 게임이 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
.
이 게시물에서 영감을 얻고 여기에 포함 된 두 봇 중 하나를 복사 할 수 있습니다. 그러나 그들이 효과적이지 않을까 걱정됩니다 ...
다른 언어 허용
샌드 박스와 The Nineteenth Byte에서 다른 언어로 제출할 수있는 방법에 대해 논의했습니다. 그러한 구현에 대해 읽고 양측의 주장을 듣고 난 후에이 과제를 Python으로 만 제한하기로 결정했습니다. 이는 여러 언어를 지원하는 데 필요한 시간과 안정성에 도달하기 위해 많은 반복이 필요한이 난제의 임의성이라는 두 가지 요인 때문입니다. 나는 당신이 여전히 참여하기를 희망하며,이 도전에 대한 파이썬을 배우고 싶다면 가능한 한 자주 채팅에서 사용할 수 있도록 노력할 것입니다.
궁금한 점이 있으면 채팅방에이 도전 과제를 적을 수 있습니다 . 거기서 보자!
규칙
- 방해 행위가 허용되고 권장됩니다. 즉, 다른 플레이어에 대한 방해
- 컨트롤러, 런타임 또는 기타 제출물을 수정하려는 시도는 실격 처리됩니다. 모든 제출물은 제공된 입력 및 저장 장치에서만 작동해야합니다.
- 500MB 이상의 메모리를 사용하여 결정을 내리는 모든 봇은 실격 처리됩니다 (많은 메모리가 필요한 경우 선택 사항을 다시 생각해야 함).
- 봇은 의도적으로 또는 실수로 기존 전략과 정확히 동일한 전략을 구현해서는 안됩니다.
- 챌린지 시간 동안 봇을 업데이트 할 수 있습니다. 그러나 접근 방식이 다른 경우 다른 봇을 게시 할 수도 있습니다.
예
class GoToTenBot(Bot):
def make_throw(self, scores, last_round):
while sum(self.current_throws) < 10:
yield True
yield False
이 봇은 라운드 적어도 10의 점수가 될 때까지 계속됩니다, 또는 당신이 6. 또한 첫 번째 던져가 6 인 경우주의 던지는 처리 할 수있는 논리를 필요로하지 않는 6 주를 throw make_throw
이다 당신의 라운드가 즉시 끝나기 때문에 전화하지 마십시오.
파이썬에 익숙하지 않고 yield
개념 에 익숙하지 않은 사람들에게 yield
키워드는 어떤 식 으로든 수익과 비슷하지만 다른 방식으로 다릅니다. 여기서 개념에 대해 읽을 수 있습니다 . 기본적으로, 일단 yield
기능이 중지되면 yield
ed 값이 컨트롤러로 다시 전송됩니다. 여기서 컨트롤러는 봇이 다른 결정을 내릴 때까지 논리를 처리합니다. 그런 다음 컨트롤러는 주사위 던지기를 보내며 make_throw
이전에 중단 된 경우 기본적으로 이전 yield
명령문 다음 줄에서 함수가 계속 실행됩니다 .
이런 식으로 게임 컨트롤러는 각 주사위 던지기마다 별도의 봇 함수 호출없이 상태를 업데이트 할 수 있습니다.
사양
에서 사용 가능한 모든 Python 라이브러리를 사용할 수 있습니다 pip
. 좋은 평균을 얻을 수 있도록 라운드 당 100 밀리 초의 시간 제한이 있습니다. 나는 당신의 스크립트가 그것보다 훨씬 빠르면 더 기뻐할 것입니다.
평가
승자를 찾기 위해 모든 봇을 가져와 무작위 그룹 8로 실행합니다. 제출 된 클래스가 8 개 미만인 경우 4 개의 무작위 그룹으로 실행하여 각 라운드에 모든 봇이 항상있는 것을 피합니다. 약 8 시간 동안 시뮬레이션을 실행하면 승자가 가장 높은 승률의 봇이됩니다. 2019 년 초에 최종 시뮬레이션을 시작하여 봇을 코딩하는 모든 크리스마스를 제공합니다! 예비 최종 날짜는 1 월 4 일이지만, 시간이 너무 적 으면 나중에 날짜를 변경할 수 있습니다.
그때까지 나는 30-60 분의 CPU 시간을 사용하고 스코어 보드를 업데이트하여 매일 시뮬레이션을 시도합니다. 이것은 공식 점수는 아니지만 어떤 봇이 가장 실적이 좋은지 확인하는 가이드 역할을합니다. 그러나 크리스마스가 다가 오면 항상 사용할 수 없다는 것을 이해할 수 있기를 바랍니다. 시뮬레이션을 실행하고 도전과 관련된 질문에 대답하기 위해 최선을 다하겠습니다.
직접 테스트
자체 시뮬레이션을 실행하려는 경우 두 개의 예제 봇을 포함하여 시뮬레이션을 실행하는 컨트롤러에 대한 전체 코드가 있습니다.
제어 장치
이 도전에 대한 업데이트 된 컨트롤러는 다음과 같습니다. AKroell 덕분에 ANSI 출력, 멀티 스레딩을 지원하고 추가 통계를 수집합니다 ! 컨트롤러를 변경하면 문서가 완료되면 게시물을 업데이트합니다.
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 구성을 사용 python forty_game_controller.py
하거나 사용 python3 forty_game_controller.py
하십시오. 시뮬레이션을 추가로 구성하려면 여기에서 지시를 따르거나 원하는 경우 코드로 땜질을 시도하십시오.
게임 통계
다른 봇을 고려하지 않고 특정 점수를 목표로하는 봇을 만드는 경우 다음이 승리 점수 백분위 수입니다.
+----------+-----+
|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
)은 규칙을 구부리도록 만들어졌으며 제작자는 공식 토너먼트에 참여하지 않기로 동의했습니다. 그러나 나는 여전히 그들의 아이디어가 창의적이라고 생각하며 존경 할만한 언급을 할 만하다. 반란군은 사보타지를 피하기 위해 영리한 전략을 사용하고 실제로 사보타주 봇이 더 잘 작동하기 때문에이 목록에 있습니다.
봇 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|
+----------+-----+