import base64
import json
import queue
from collections import defaultdict
from datetime import datetime
from itertools import product
from threading import Thread
from pytz import timezone
from common.db import connect_db
from logger import log
from runner import score
from thread_utils import only_once
NUM_WORKERS = 4
LOG_MOD = 1
THRESHOLD = 0.500001
last_updated = "the end of the tournament." # "unknown"
[docs]def post_tournament():
"""
RANKINGS and WINRATES are nonlocal variables
Connect to the database. Fetch all name and hash pairs from the CACHED_STRATEGIES
table (Call this hash_lookup). Fetch all winrates from CACHED_WINRATES table (call
this db_winrates).
Create a nested defaultdict for the winrate and a defaultdict for num rates.
Create a 2d array from the data in hash loopup, where each data point is a [name, hash]
pair and call this new data structure ACTIVE_TEAMS.
Iterate through db_winrates and add the data into the nested WINRATE_DICT
(data: h0, h1, rate) such that it follows the following convention:
- WINRATE_DICT[h0][h1] = winrate
- WINRATE_DICT[h1][h0] = 1 - winrate
- WINRATE_DICT[h0][h0] = 0.5
- WINRATE_DICT[h1][h1] = 0.5
Have two teams play against each other. If the winrate of team0 agains team1 is
greater that THRESHOLD, then add 1 win to the num_teams dict for team0.
Construct a new array for teams data consisting of [team name decoded, number of wins, and the hash].
Sort the teams by number of wins, and construct a ranking.
Create a 2-d Matrix of WINRATES of each team playing each other.
:return: None
"""
log("Updating website...")
global ranking, winrates
with connect_db() as db:
hash_lookup = db("SELECT name, hash FROM cached_strategies").fetchall()
db_winrates = db(
"SELECT hash_0, hash_1, winrate FROM cached_winrates"
).fetchall()
winrate_dict = defaultdict(lambda: defaultdict(float))
num_wins = defaultdict(int)
active_teams = []
for name, hash in hash_lookup:
active_teams.append([name, hash])
for hash_0, hash_1, winrate in db_winrates:
winrate_dict[hash_0][hash_1] = winrate
winrate_dict[hash_1][hash_0] = 1 - winrate
winrate_dict[hash_0][hash_0] = 0.5
winrate_dict[hash_1][hash_1] = 0.5
for team_0, hash_0 in active_teams:
for team_1, hash_1 in active_teams:
if winrate_dict[hash_0][hash_1] > THRESHOLD:
num_wins[team_0] += 1
teams = []
for name, hash in hash_lookup:
teams.append([base64.b64decode(name).decode("utf-8"), num_wins[name], hash])
teams.sort(key=lambda x: x[1], reverse=True)
ranking = build_ranking(teams)
winrates = []
for _, _, hash_0 in teams:
winrates.append([])
for _, _, hash_1 in teams:
winrates[-1].append(winrate_dict[hash_0][hash_1])
[docs]def build_ranking(teams):
"""
Assigns rankings to the teams based on their winrates. If teams are tied,
they will be assigned the same ranking as each other.
:param teams: A list of teams, where each element is a list containing a team's name
and winrate (among potentially other data). This list is already sorted by num wins in descending order.
:type teams: list
:return: List of teams with their rank, name, and number of wins, in ascending order of rank.
"""
out = []
prev_wins = float("inf")
curr_rank = -1
cnt = 0
for name, wins, *_ in teams:
cnt += 1
if wins < prev_wins:
curr_rank += cnt
prev_wins = wins
cnt = 0
out.append([curr_rank, name, wins])
return out
[docs]def worker(t_id, q, out, goal):
"""
We get each task from the queue. Score the strategies.
Store the score in the OUT dictionary.
Finish the task.
If the size of our queue is a factor of LOG_MOD, then we log the number of matches completed.
:param t_id: Id number
:type t_id: int
:param q: Queue of tasks
:type q: Queue
:param out: data structure of the storing the score between strategies.
:type out: dictionary
:param goal: Size of the queue of tasks. AKA Number of tasks.
:type goal: int
:return: None
"""
while True:
task = q.get()
if task is None:
break
hash_0, strat_0, hash_1, strat_1 = task
log("Thread {}: Matching {} vs {}".format(t_id, hash_0, hash_1))
out[hash_0, hash_1] = score(strat_0, strat_1)
log("Thread {} finished match.".format(t_id))
q.task_done()
if q.qsize() % LOG_MOD == 0:
log("{} / {} matches complete".format(goal - q.qsize(), goal))
[docs]def unwrap(strat):
"""
Return the hash of the strat, and load the strategy from json.
:param strat: data structure containing data pertaining to the strategy.
:type strat: dictionary
:return: tuple of hash and the strategy loaded from json.
"""
return strat["hash"], json.loads(strat["strategy"])
[docs]@only_once
def run_tournament():
"""
Connects to database, and fetches data from CACHED_STRATEGIES and CACHED_WINRATES.
Stores the current time as START_TIME.
Store the data from CACHED_WINRATES in a dictionary called WINRATES.
Create a QUEUE for tasks.
Iterate through entry0, entry1 in the Cartesian product of ALL_STRATEGIES and
ALL_STRATEGIES. If hash of entry0 is greater than or equal to the hash of entry1 move
onto the next entry pair. If the tuple of the hashes of entry0 and entry1 are in the
WINRATES dictionary, then add 1 to the NUM_DONE variable, and move onto the next entry pair.
If neither of the former two cases are true, then call unwrap on both of the entries and put
the output of both calls in a list and put this list in the tasks queue.
Create a dictionary called OUT, and a list called THREADS.
Create a THREAD where the target=worker, and pass in the arguments T_ID, the queue of TASKS,
the OUT dictionary, and SIZE of the TASKS queue.
Start each process. Then append to the THREADS list.
Join the TASKS structure.
Add end-of-queue markers to the TASKS structure.
Store the newly computed winrates into the CACHED_WINRATES data table.
Update the LAST_UPDATED time with the START_TIME. Call
the POST_TOURNAMENT function.
"""
global last_updated
with connect_db() as db:
all_strategies = db("SELECT hash, strategy FROM cached_strategies").fetchall()
cached_winrates = db(
"SELECT hash_0, hash_1, winrate FROM cached_winrates"
).fetchall()
start_time = datetime.now().astimezone(timezone("US/Pacific"))
log("Starting tournament with frozen copy of strategies...")
winrates = {}
for hash_0, hash_1, winrate in cached_winrates:
winrates[hash_0, hash_1] = winrate
tasks = queue.Queue()
num_done = 0
for entry_0, entry_1 in product(all_strategies, all_strategies):
if entry_0["hash"] >= entry_1["hash"]:
continue
if (entry_0["hash"], entry_1["hash"]) in winrates:
num_done += 1
continue
tasks.put([*unwrap(entry_0), *unwrap(entry_1)])
num_todo = tasks.qsize()
log(
"{} matches recovered from cache, {} to be recomputed".format(
num_done, num_todo
)
)
out = {} # access is thread-safe since we're always writing to diff. keys
threads = []
for i in range(NUM_WORKERS):
t = Thread(target=worker, args=(i, tasks, out, num_todo))
log("Starting thread {}...".format(i))
t.start()
threads.append(t)
tasks.join()
log("Tournament finished, storing results...")
for i in range(NUM_WORKERS):
tasks.put(None)
with connect_db() as db:
for (hash_0, hash_1), winrate in out.items():
db(
"INSERT INTO cached_winrates VALUES (%s, %s, %s)",
[hash_0, hash_1, winrate],
)
last_updated = start_time
post_tournament()
log("Website updated")