トーナメント終了!
トーナメントは終了しました!最終シミュレーションは夜に実行され、合計ゲームでした。勝者は、ボットOptFor2Xを使用したChristian Sievers です。クリスチャンシーバーズは反乱軍と2位を確保することもできました。おめでとうございます!以下に、トーナメントの公式ハイスコアリストを見ることができます。
それでもゲームをプレイしたい場合は、以下に掲載されているコントローラーを使用し、その中のコードを使用して独自のゲームを作成してください。
私は聞いたことがなかったサイコロのゲームをプレイするように招待されました。ルールはシンプルでしたが、KotHチャレンジには最適だと思います。
ルール
ゲームの始まり
ダイスはテーブルを一周し、自分の番になるたびに、好きなだけダイスを投げることができます。ただし、少なくとも1回はスローする必要があります。ラウンドのすべてのスローの合計を追跡します。停止することを選択した場合、ラウンドのスコアが合計スコアに追加されます。
だから、なぜあなたはダイを投げるのをやめるのですか?あなたが6を取得した場合、ラウンド全体のスコアがゼロになり、ダイスが渡されるためです。したがって、最初の目標は、できるだけ早くスコアを上げることです。
誰が勝ちましたか?
テーブルの周りの最初のプレーヤーが40ポイント以上になると、最後のラウンドが始まります。最後のラウンドが始まると、最後のラウンドを開始した人以外の全員がもう1ターンを獲得します。
最終ラウンドのルールは、他のラウンドと同じです。投げ続けるか、停止するかを選択します。ただし、最終ラウンドの前のスコアよりも高いスコアを取得しないと、勝つチャンスがないことを知っています。しかし、あなたがあまりにも遠くに行き続けるなら、あなたは6を得るかもしれません。
ただし、考慮すべきルールがもう1つあります。現在の合計スコア(以前のスコア+ラウンドの現在のスコア)が40以上で、6をヒットした場合、合計スコアは0に設定されます。つまり、最初からやり直す必要があります。現在の合計スコアが40以上のときに6を押した場合、ゲームは通常通り続行しますが、現在は最後の場所にいます。合計スコアがリセットされても、最終ラウンドはトリガーされません。あなたはまだラウンドに勝つことができますが、それはより挑戦的になります。
勝者は、最後のラウンドが終了した時点で最高のスコアを獲得したプレーヤーです。2人以上のプレイヤーが同じスコアを共有する場合、それらはすべて勝利者としてカウントされます。
追加のルールは、ゲームが最大200ラウンド継続することです。これは、複数のボットが現在のスコアを維持するために6を押すまで基本的に投げ続けるケースを防ぐためです。199回目のラウンドに合格すると、last_round
trueに設定され、さらに1ラウンドがプレイされます。ゲームが200ラウンドになった場合、最高得点のボット(またはボット)が勝者となります(40ポイント以上を持っていなくても)。
要約
- 停止するか6を得るまで、各ラウンドでダイスを投げ続けます。
- ダイスを1回投げる必要があります(最初の投げが6の場合、ラウンドはすぐに終わります)。
- 6を取得した場合、現在のスコアは0に設定されます(合計スコアではありません)
- 各ラウンドの後に、現在のスコアを合計スコアに追加します
- ボットがターンを終了し、合計スコアが少なくとも40になると、他の全員が最後のターンを獲得します
- 現在の合計スコアがで6を獲得した場合、合計スコアは0に設定され、ラウンドは終了します。
- 上記の場合、最終ラウンドはトリガーされません
- 最終ラウンド後の合計スコアが最も高い人が勝者です
- 複数の勝者がいる場合、すべてが勝者としてカウントされます
- ゲームは最大200ラウンド続きます
スコアの明確化
- 合計スコア:前のラウンドで保存したスコア
- 現在のスコア:現在のラウンドのスコア
- 現在の合計スコア:上記の2つのスコアの合計
どうやって参加しますか
この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
。
この投稿から自由にインスピレーションを求め、ここに含めた2つのボットのいずれかをコピーしてください。しかし、私はそれらが特に効果的ではないことを恐れています...
他の言語の許可について
サンドボックスとThe Nineteenth Byteの両方で、他の言語での投稿を許可することについて議論しました。そのような実装について読んで、両側から議論を聞いた後、私はこの挑戦をPythonだけに制限することに決めました。これは、2つの要因によるものです。複数の言語をサポートするのに必要な時間と、安定性に達するまでに多数の反復を必要とするこの課題のランダム性です。引き続き参加してください。このチャレンジのために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である場合ことに注意して投げまで、このボットは行き続けるだろう、make_throw
ですラウンドはすぐに終了するため、呼び出されません。
Pythonが初めて(およびyield
コンセプトが初めて)でも、これを試してみたい人にとって、yield
キーワードはある意味でリターンに似ていますが、他の方法では異なります。コンセプトについてはこちらをご覧ください。基本的に、一度を実行yield
すると関数が停止し、yield
編集した値がコントローラーに返送されます。コントローラーは、ボットが別の決定を下すときまで、そのロジックを処理します。その後、コントローラーはサイコロを送信し、make_throw
関数は前に停止した場合、基本的に前のyield
ステートメントの後の行で実行を続けます。
このようにして、ゲームコントローラは、サイコロを投げるたびに個別のボット関数呼び出しを必要とせずに状態を更新できます。
仕様
で利用可能な任意のPythonライブラリを使用できますpip
。良い平均を得るために、ラウンドごとに100ミリ秒の時間制限があります。あなたのスクリプトがそれよりもはるかに高速で、もっと多くのラウンドを実行できるようになれば本当に嬉しいです。
評価
勝者を見つけるために、すべてのボットを8個のランダムグループで実行します。送信されるクラスが8個未満の場合、各ラウンドですべてのボットが常に存在することを避けるために、4個のランダムグループで実行します。約8時間シミュレーションを実行し、勝者は勝率が最も高いボットになります。2019年の初めに最終シミュレーションを開始し、クリスマスのすべてをボットのコーディングに提供します!暫定最終日は1月4日ですが、時間が足りない場合は後日に変更できます。
それまでは、30〜60分のCPU時間を使用し、スコアボードを更新して、毎日のシミュレーションを試みます。これは公式のスコアではありませんが、どのボットが最高のパフォーマンスを発揮するかを知るためのガイドとして役立ちます。しかし、クリスマスが近づいているので、私がいつでも利用できるわけではないことを理解していただければ幸いです。シミュレーションを実行し、チャレンジに関連する質問に答えるために最善を尽くします。
自分でテストする
独自のシミュレーションを実行する場合、2つのサンプルボットを含む、シミュレーションを実行するコントローラーの完全なコードを次に示します。
コントローラ
このチャレンジ用に更新されたコントローラーは次のとおりです。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
シミュレーションを実行する
シミュレーションを実行するには、上記の両方のコードスニペットを2つの個別のファイルに保存します。forty_game_controller.py
およびとして保存しましたforty_game_bots.py
。次に、Pythonの構成を使用する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のおかげで、各ボットの統計情報が増えました!3つの新しい列には、すべてのゲームの最大スコア、ゲームごとの平均スコア、および各ボットで勝ったときの平均スコアが含まれています。
コメントで指摘されているように、ゲーム内でより高いインデックスを持っているボットが場合によっては余分なラウンドを取得するゲームロジックに問題がありました。これは現在修正されており、以下のスコアはこれを反映しています。
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|
+----------+-----+