Last active
December 28, 2015 17:19
-
-
Save icook/7534827 to your computer and use it in GitHub Desktop.
A thread based runner for robot game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is a thread based comparison runner for robot game. Essentially it takes the latest | |
x number of scripts in a folder called "engines" and runs every combination against eachother | |
repeatedly. It shows simple W/L ratio to help figure out which algorithms work the best. | |
""" | |
import curses | |
import path | |
import sys | |
import threading | |
import traceback | |
import datetime | |
import pprint | |
import os | |
import ast | |
import game | |
import itertools | |
from tabulate import tabulate | |
from settings import settings | |
from time import sleep | |
def make_player(fname): | |
return game.Player(open(fname).read()) | |
# how many files to run together | |
latest_count = 5 | |
class GameRunner(threading.Thread): | |
""" Run a single instance of the robot game against two ai engines """ | |
def __init__(self, run_record, map_data): | |
super(GameRunner, self).__init__() | |
self._stop = threading.Event() | |
self.run_record = run_record | |
self.map_data = map_data | |
self.timer = {} | |
def st(self, *args): | |
for a in args: | |
self.timer[a] = datetime.datetime.now() | |
def gt(self, key): | |
""" update an average for this timing sample """ | |
# get the time sample | |
now = datetime.datetime.now() | |
rt = (now - self.timer[key]).microseconds | |
self.timer[key] = now | |
# update the average with a moving average value (avoids keeping a list) | |
ct = self.run_record[key + 's'] | |
avg = self.run_record['avg_' + key] | |
self.run_record['avg_' + key] = ((ct * avg) + rt) / (ct + 1) | |
self.run_record[key + 's'] += 1 | |
def run(self): | |
game.init_settings(self.map_data) | |
self.st('turn') | |
while not self._stop.is_set(): | |
players = [make_player(self.run_record['p1']['filename']), | |
make_player(self.run_record['p2']['filename'])] | |
g = game.Game(*players, record_turns=False) | |
for i in range(settings.max_turns): | |
g.run_turn() | |
self.gt('turn') | |
scores = g.get_scores() | |
if scores[0] > scores[1]: | |
self.run_record['p1']['wins'] += 1 | |
self.run_record['p2']['losses'] += 1 | |
else: | |
self.run_record['p1']['losses'] += 1 | |
self.run_record['p2']['wins'] += 1 | |
self.run_record['games'] += 1 | |
def stop(self): | |
self._stop.set() | |
def main(stdscr, threads): | |
# Clear screen | |
stdscr.clear() | |
# map data to be used in workers | |
map_name = os.path.join(os.path.dirname(__file__), 'maps/default.py') | |
map_data = ast.literal_eval(open(map_name).read()) | |
# record all details of workers | |
run_records = [] | |
# get the most recently modified x objects | |
d = path.path('./engines/').files('*.py') | |
d.sort(key=lambda x: x.mtime, reverse=True) | |
latest = d[:latest_count] | |
# create a thread for each combination and tally results | |
i = 0 | |
for p1, p2 in itertools.combinations(latest, 2): | |
dct = {'p1': {'filename': p1, | |
'wins': 0, | |
'losses': 0}, | |
'p2': {'filename': p2, | |
'wins': 0, | |
'losses': 0}, | |
'avg_turn': 1, | |
'turns': 0, | |
'start': datetime.datetime.now(), | |
'games': 0 | |
} | |
run_records.append(dct) | |
thread = GameRunner(run_records[i], map_data) | |
thread.start() | |
run_records[i]['thread'] = thread | |
threads.append(thread) | |
i += 1 | |
while True: | |
data = {'P1 fname': [], | |
'P2 fname': [], | |
'P1 W/L': [], | |
'P2 W/L': [], | |
'Games': [], | |
'Turns/Sec': [], | |
'Turns': [], | |
'Seconds Running': [], | |
'Overall Turns/Sec': [], | |
} | |
for val in run_records: | |
p1 = val['p1'] | |
p2 = val['p2'] | |
try: | |
wl1 = float(p1['wins']) / p1['losses'] | |
except ZeroDivisionError: | |
wl1 = -1 | |
try: | |
wl2 = float(p2['wins']) / p2['losses'] | |
except ZeroDivisionError: | |
wl2 = -1 | |
secs = (datetime.datetime.now() - val['start']).seconds | |
if secs == 0: secs = 1 | |
data['P1 fname'].append(p1['filename']) | |
data['P2 fname'].append(p2['filename']) | |
data['P1 W/L'].append(wl1) | |
data['P2 W/L'].append(wl2) | |
data['Games'].append(val['games']) | |
data['Turns/Sec'].append(1000000 / val['avg_turn']) | |
data['Turns'].append(val['turns']) | |
data['Overall Turns/Sec'].append(val['turns'] / secs) | |
data['Seconds Running'].append(secs) | |
tab = tabulate(data, headers="keys", floatfmt=".4f").replace("\n", "\r") | |
for i, t in enumerate(tab.splitlines(True)): | |
stdscr.addstr(i, 0, t) | |
stdscr.refresh() | |
sleep(1) | |
if __name__ == "__main__": | |
threads = [] | |
try: | |
# Initialize curses | |
stdscr = curses.initscr() | |
# Turn off echoing of keys, and enter cbreak mode, | |
# where no buffering is performed on keyboard input | |
curses.noecho() | |
curses.cbreak() | |
# In keypad mode, escape sequences for special keys | |
# (like the cursor keys) will be interpreted and | |
# a special value like curses.KEY_LEFT will be returned | |
stdscr.keypad(1) | |
# Start color, too. Harmless if the terminal doesn't have | |
# color; user can test with has_color() later on. The try/catch | |
# works around a minor bit of over-conscientiousness in the curses | |
# module -- the error return from C start_color() is ignorable. | |
try: | |
curses.start_color() | |
except: | |
pass | |
main(stdscr, threads) | |
finally: | |
traceback.print_exc(file=sys.stdout) | |
# Set everything back to normal | |
if 'stdscr' in locals(): | |
stdscr.keypad(0) | |
curses.echo() | |
curses.nocbreak() | |
curses.endwin() | |
for t in threads: | |
t.stop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment