''' Created on Jun 13, 2015 @author: Brett Paufler Copyright Brett Paufler Intent GA_ABM Use Genetic Algorithms (programming) to direct an agent based model focusing on the organic development of football plays Stopped Working on this version 6-27-15 Data Types too complex ''' import time import random import math import os import numpy as np import moviepy.editor as mpy import scipy.misc from collections import namedtuple from itertools import groupby #from shutil import rmtree from skimage.io import imread from copy import deepcopy from skimage.draw import line #, bezier_curve class Player(): '''Roster loads Players into Scrimmage in Scrimmage every Player is a separate instance''' def __init__(self, max_delta=1.0, max_velocity=5.0): self.x = 0.0 self.y = 0.0 self.vx = 0.0 self.vy = 0.0 self.max_delta = max_delta #change in velocity self.max_velocity = max_velocity self.plays = [] self.offense = True self.history_mutations = [] self.history_moves = [] def __repr__(self): p = 'Player Object (id#): %s - %s\n' % (id(self), str(self.offense)) p += '\tx, y: \t%.4f, %.4f\r' % (self.x, self.y) p += '\tvx, vy:\t%.4f, %.4f\r' % (self.vx, self.vy) p += '\tmax_delta, max_velocity:\t%.4f, %.4f\r' % (self.max_delta, self.max_velocity) p += '\tCURRENT PLAYS:\r' #p += '\t\t%s' % len(self.plays) for play in self.plays: p += '\t\t%s, %.4f, %.4f, %d\r' % (play[0], play[1], play[2], play[3]) p += '\t\tMoves: %s' % str(self.history_moves) p += '\n' #p += '\tHistory: %s' % str(self.hist_mutations) return p def distance(self, other): '''returns distance between self and other players''' vector = np.array([self.x, self.y]) - np.array([other.x, other.y]) return np.linalg.norm(vector) def diff_vel(self, other): '''returns velocity difference between self and other player''' vector = np.array([self.vx, self.vy]) - np.array([other.vx, other.vy]) return np.linalg.norm(vector) def normalize(self, x, y, max_value): combined = math.sqrt(pow(x, 2) + pow(y, 2)) if combined > max_value: x = x * max_value / combined y = y * max_value / combined return (x, y) def normalize_velocity(self): '''reduces velocity to player.max_velocity''' self.vx, self.vy = self.normalize(self.vx, self.vy, self.max_velocity) def return_normalized_delta(self, x, y): '''returns provided (x, y) to player.max_delta, max_delta is the max change to velocity any turn ''' return self.normalize(x, y, self.max_delta) def apply_delta(self, a, b): '''applies change in velocity delta (a,b) to velocity taking into account player.max_delta, player.max_velocity''' a, b = self.return_normalized_delta(a, b) self.vx += a self.vy += b self.normalize_velocity() #PLAYS Subject to MutaGenetics def play_nada(self, a, b, s): '''empty play''' pass def play_start(self, a, b, s): '''players starting position on field all play_sets must start with a 'play_start' type play''' self.x = a self.y = b def play_change_v(self, a, b, s): '''applies inputs to vx & vy''' self.apply_delta(a, b) #a, b = self.return_normalized_delta(a, b) #self.vx += a #self.vy += b #self.normalize_velocity() def play_pass_coverage_closest(self, a, b, scrimmage): '''player attempts to close with closest opponent math is mostly gobbledegook and could be changed''' d = 500.0 for r in [s for s in scrimmage.players if s.offense != self.offense]: dis = self.distance(r) if dis < d: d = dis #Same Old Formula x = (r.x - self.x) * pow(d/5.0,4) + (r.vx - self.vx) y = (r.y - self.y) * pow(d/5.0,4) + (r.vy - self.vy) self.apply_delta(x, y) def play_book(self): '''returns dictionary of known plays {f.__name: f, ... for all plays}''' all_plays = [self.play_change_v, self.play_nada, self.play_pass_coverage_closest, self.play_start, ] play_names = [p.__name__ for p in all_plays] return dict(zip(play_names, all_plays)) def load_plays(self, playlist): '''transforms a string list of play names into function calls string comes from a Roster player_stats.play_set list''' book = self.play_book() for play in playlist: if play[0] in book: self.plays.append([book[play[0]], play[1], play[2], play[3]]) else: raise SyntaxError('No Matching Play in play_book(): %s' % play[0]) class Scrimmage(): '''Where the Scrimmages are run''' def __init__(self, max_turns=25, define_open=2.5, field_width=20, field_backfield=5, field_front=15, check_proximity=True, max_turns_always=False): '''Heavy lifting of initialization is handled by Roster Class''' #OPTIONS self.check_proximity = check_proximity self.max_turns_always = max_turns_always self.define_open = define_open self.max_turns = max_turns self.turn_number = 0 self.field_width = field_width self.field_backfield = field_backfield self.field_front = field_front self.players = [] self.images = [] self.rating = 'X' # Forces error self.hist_mutations = ['rand_start'] def __repr__(self): t = '\nSCRIMMAGE REPORT: %d turns\n' % self.max_turns for player in self.players: t += str(player) t += '\n' return t def player_mask(self, rotation=0, offense=True, size=(10,10)): '''returns a player graphical mask offense True=red, False=blue rotation is degrees clockwise (hands upward at zero) ''' body_mask = np.array([ [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,1,1,1,1,1,1,1,0,0], [0,1,1,1,0,0,0,1,1,1,0], [0,1,1,0,0,1,0,0,1,1,0], [0,1,1,1,0,0,0,1,1,1,0], [0,0,1,1,1,1,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], ]) torso_mask = np.array([ [0,0,0,0,0,0,0,0,0,0,0], [0,0,1,1,0,0,0,1,1,0,0], [0,0,0,0,1,1,1,0,0,0,0], [0,0,0,1,1,0,1,1,0,0,0], [0,0,1,1,0,1,0,1,1,0,0], [0,0,0,1,1,0,1,1,0,0,0], [0,0,0,0,1,1,1,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], ]) head_mask = np.array([ [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,1,1,1,0,0,0,0], [0,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0,0], ]) if offense: #red mask = np.dstack((body_mask, torso_mask, head_mask)) else: #blue mask = np.dstack(( torso_mask, head_mask, body_mask)) mask = scipy.misc.imresize(mask, size) mask = scipy.misc.imrotate(mask, -rotation) return mask def text_to_image(self, text_in = "Nothing \n So Far", size=(500,500,3)): '''converts string to title.png saved in ./temp''' #Text File w,h,_ = size top_line = '(c)' + (" " * int(w/8.33 - 6)) + '(c)\n' #old was 58 bottom_line = '(ran %s)\n(c) Brett Paufler' % time.strftime('%m-%d-%Y') text = top_line + text_in text = text + '\n' * (int(h/15.63) - text.count('\n') - 1) + bottom_line #old was 32 with open('./temp/text.txt', 'w') as f: f.write(text) #Text to .png irfanview_path = r'"C:\Program Files (x86)\IrfanView\i_view32.exe" ' text_file_in = r'TODO ' irfanview_commands = r' /swap_bw /convert=' #% (w,h) img_file_out = r'C: TODO title.png' command = irfanview_path + text_file_in + irfanview_commands + img_file_out print command os.system(command) print "CREATED: title.png" def load_roster(self, list_of_player_stats): '''Loads a list of player_stat namedtuples Note: player_stat.move_history is ignored as it is dynamically updated''' for p in list_of_player_stats: player = Player() player.offense = p.offense player.load_plays(p.play_set) self.players.append(player) def offensive_players(self): '''returns list of offensive players''' return [p for p in self.players if p.offense == True] def eligible_receivers(self): '''returns a list of offensive players across the line of scrimmage''' return [p for p in self.offensive_players() if p.y > 0] def defensive_players(self): '''returns list of defensive players''' return [p for p in self.players if p.offense == False] def turn_update_plays(self): '''logic for implementing the next play in the players.plays''' for player in self.players: if player.plays: player.plays[0][0](player.plays[0][1], player.plays[0][2], self) if player.plays[0][3] > 0: player.plays[0][3] -= 1 else: player.plays = player.plays[1:] def turn_check_proximity(self): '''optional reduction of vx,vy if ANY other player is close by''' for p1 in self.players: for p2 in [p2 for p2 in self.players if p1 != p2]: d = p1.distance(p2) if d < 1.0: p1.vy *= d p1.vy *= d def turn_update_movement(self): for player in self.players: player.x += player.vx player.y += player.vy player.history_moves.append((player.x, player.y, player.vx, player.vy)) def turn(self): self.turn_update_plays() if self.check_proximity: self.turn_check_proximity() self.turn_update_movement() self.turn_number += 1 def end_turn(self): '''returns False until Scrimmage 'end' condition met currently by turn_count, open_receiver...''' #Gawd this is a nasty piece of code logic for my humble mind end_turn_count = self.turn_number >= self.max_turns if self.max_turns_always: check = end_turn_count else: end_open_receiver = 0 != len(self.open_receiver_list()) #Add any future test BOTH above and below this comment line #If test above returns True, function returns False, and turn ends check = any([end_turn_count, end_open_receiver ]) return not check #Returns False to End Turn, if any above check is True def run_scrimmage(self, rate=True): '''runs Scrimmage until end_turn() returns False''' while self.end_turn(): self.turn() if rate: self.rate_it() def is_receiver_open(self, p1): '''Truth Test: is this_receiver(p1) BOTH downfield and open''' down_field = p1 in self.eligible_receivers() covered = any([p1.distance(p2) < self.define_open for p2 in self.defensive_players()]) return (not covered) and down_field def open_receiver_list(self): '''list of any eligible receivers that are open''' players_list = [p for p in self.eligible_receivers() if self.is_receiver_open(p)] return players_list def img_blank(self): '''returns a blank (all zeros) np.array 10x the size of the field''' img = np.zeros((10 * (self.field_front + self.field_backfield), 10 * self.field_width, 3), dtype=int) return img def player_to_field(self, x, y): '''converts a player location to image location player metrics are 1:1, and centered on ball field metrics are 10:1 and centered per an image, upper left''' new_y = int(self.field_front * 10 - y * 10) new_x = int(self.field_width * 10 / 2 + x * 10) return (new_x, new_y) def draw_img_history(self): '''updates self.images=[img,img,img...blank, title, blank] where img are successive screenshots of the play no .png, .jpg, or .gif is made, self.images=list(img,img...) only''' num_moves = len(self.players[0].history_moves) length_equal = all([(len(p.history_moves) == num_moves) for p in self.players]) if not length_equal: raise Exception('SERIOUS ERROR: Not all player moves being recorded') #Base & Center Dot img_base = self.img_blank() img_base[10 * self.field_front - 1 : 10 * self.field_front + 1, 10 * self.field_width/2 - 1 : 10 * self.field_width/2 + 1] = 255 for i in range(num_moves): img = np.copy(img_base) for player in self.players: #Drawing the Players x = player.history_moves[i][0] y = player.history_moves[i][1] vx = player.history_moves[i][2] vy = player.history_moves[i][3] #Rotation #Start is a bit of a hack, facing doesn't change until player moves #offense hard coded up, defense down start = (i == 0) or all([(s[0] == player.history_moves[0][0]) and (s[1] == player.history_moves[0][1]) for s in player.history_moves[:i]]) if start: if player.offense == True: rotation = 0 else: rotation = 180 else: rotation = math.degrees(math.atan2(vy, -vx)) - 90 #Player Placement (notice the call function) x, y = self.player_to_field(x, y) if y - 5 > 0 and y + 5 < img.shape[1] - 1: if x - 5 > 0 and (x + 5 < img.shape[0] - 1): img[y-5:y+5, x-5:x+5] += self.player_mask(rotation, player.offense) #Drawing the White Box around open receivers if self.is_receiver_open(player) and (i == num_moves - 1): #print i, self.is_receiver_open(player) img[y-5, x-5:x+5] += 255 img[y+5, x-5:x+5] += 255 img[y-5:y+5, x-5] += 255 img[y-5:y+5, x+5] += 255 #Historical Movement Tracks #Offense=Red, Defense=Blue x_y = [] for move in player.history_moves[:i]: lx, ly, _ ,__ = move lx, ly = self.player_to_field(lx, ly) if 0 < lx < img.shape[1] - 1 and 0 < ly < img.shape[0] - 1: x_y.append((int(ly), int(lx))) if 0 < x < img.shape[1] - 1 and 0 < y < img.shape[0] - 1: x_y.append((y,x)) for j in range(len(x_y) - 1): rr, cc = line(x_y[j][0], x_y[j][1], x_y[j+1][0], x_y[j+1][1]) if player.offense: img[rr,cc,0] = 255 else: img[rr,cc,2] = 255 img = np.array(img, dtype=int) self.images.append(img) self.images.append(img) self.images.append(img) def images_add_blankscreen(self): '''adds an all black img to images=[]''' self.images.append(self.img_blank()) def images_add_titlescreen(self, text='', screen_beats=1): '''adds a title screen img to images=[], screen_beats indicated how many beats for img''' if not text: text = '!!! Pass Complete !!!' text = text.rjust(int(self.field_width*10/16.33) + len(text)/2, ' ') text = '\n\n\n%s\n\n%s\n\n%s' % (text,text,text) self.text_to_image(text, self.img_blank().shape) img = imread('temp/title.png') img = scipy.misc.imresize(img, self.img_blank().shape) img = np.array(np.dstack([img, img, img]), dtype=int) for _ in range(screen_beats): self.images.append(img) def gif_it(self, sN, title_text): '''outputs scrimmage as gif sN = name for gif title_text = what does it say ''' #self.create_temp_dir() #All in All, this might be a mistake self.draw_img_history() self.images_add_blankscreen() self.images_add_titlescreen(title_text, screen_beats=5) self.images_add_blankscreen() clip = mpy.ImageSequenceClip(self.images, fps=5) #fps=??? print sN clip.write_gif(sN) print "GIF IT FINISHED: %s" % sN def points_offensive_line_up_spacing(self): '''is the offense spread out at start returns binary 0 or 1''' points = 1.0 for p1 in self.offensive_players(): for p2 in [p2 for p2 in self.offensive_players() if p2 != p1]: if abs(p1.x - p2.x) < 1.0 or abs(p1.y - p2.y) < 1.0: points = 0.0 return points def points_open_receivers(self): '''1 per open receiver''' points = sum([1 for _ in self.open_receiver_list()]) return points def points_turns(self): '''quicker is better''' points = 1 + self.max_turns - self.turn_number return points def points_down_field(self): '''This number can be relatively large''' points = sum([p.y for p in self.open_receiver_list()]) return points def points_receiver_in_center(self): '''Another high point category, distance from center for all receivers''' points = sum([self.field_width/2 - abs(p.x) for p in self.open_receiver_list()]) return points def points_receiver_on_field(self): '''Points if receivers stay on the field the 2 is to keep image on board, hopefully''' points = float(all([p.y < self.field_front - 2 for p in self.open_receiver_list()])) return points def points_all_on_field(self): '''One point for each player on field at end''' points = len([p for p in self.players if abs(p.x) - 2 < self.field_width and p.y < self.field_front - 2 and p.y > self.field_backfield + 2]) return points def points_all_always_on_field(self): '''0 if any player ever leaves the field, 1 otherwise''' points = 1.0 for player in self.players: for move in player.history_moves: x, y, _, __ = move if not (0 <= abs(x) <= self.field_width/2): if not (self.field_backfield <= y <= self.field_front): points = 0.0 return points def rate_it(self): '''Rates the Scrimmage, assigning a float value rating to self.rating >= 0''' points = (self.points_turns() * self.points_offensive_line_up_spacing() * self.points_open_receivers() * self.points_down_field() * self.points_receiver_in_center() * self.points_receiver_on_field() * self.points_all_on_field() * self.points_all_always_on_field() ) self.rating = points self.hist_mutations[-1] += '_%d' % int(self.rating) class Roster(): '''A list of player_stat named tupes ('offense', 'play_set', 'move_history') Differs from Players in a Scrimmage in the play_set is a list of strings ''' player_stats = namedtuple('player_stats', 'offense play_set move_history') def __init__(self, roster, hist_mutations): '''creates a Roster object roster takes [] or list of player_stats hist_mutations takes ['rand_roster'] at a minimum ''' self.playbook = Player().play_book().keys() self.field_width = Scrimmage().field_width self.max_play_duration = 5 self.max_plays_in_play_set = 5 self.offense_max_setback = 3.0 self.roster = roster # [player_stats, player_stats, ...] self.hist_mutations = hist_mutations # ['m_name_score', 'm_name_score', ...] def __repr__(self): t = '\nROSTER:\n' for r in self.roster: t += '\toffense: %s\n' % str(r.offense) t += '\tplay_set:\n' for p in r.play_set: t += '\t\t%s\n' % str(p) return t def set_defense(self): '''Initializes Non-Variable Defense for Roster defenders are given the same type of defensive structure with no learning, growth, or change''' offense = [p for p in self.roster if p.offense==True] defense = [] for p in offense: defense.append(self.player_stats(offense=False, play_set=[['play_start', p.play_set[0][1], 0.5, 0], ['play_pass_coverage_closest', 0.0, 0.0, 100]], move_history=[])) self.roster = offense + defense def rand_position_width(self): '''random number form -1/2 field width to 1/2 field width ball is centered at (0,0)''' return (2.0 * random.random() - 1.0) * float(self.field_width) / 2.0 def rand_setback(self, offense=True): '''returns random number -0.5 to -self.offense_max_setback (or pos number in same range if offense=False) ball no man's land is considered to be 1 wide, so 0.5 for each side from Line of Scrimmage at 0.0''' r_s = 0.5 + (self.offense_max_setback - 0.5) * random.random() # 0.5 to set_back r_s = r_s * (-2 * offense + 1) # + or - previous return r_s def rand_a(self): '''returns random number from -1.0 to +1.0''' return 2 * random.random() - 1 def rand_max_play(self): '''returns a random number from 0 to the preset maximum length of a play''' return random.randrange(self.max_play_duration) def rand_abc(self): '''Returns random a, b, & c numbers as a tuple player_stats.play_set are composed of ('name', a(float), b(float), c(int))''' a = self.rand_a() b = self.rand_a() c = self.rand_max_play() return (a,b,c) def rand_play(self): '''returns a fully formated random play([name, a, b, c]) excepting for the 'play_start' type''' play_types = [p for p in self.playbook if p != 'play_start' ] play_type = random.choice(play_types) a,b,c = self.rand_abc() return [play_type, a, b, c] def rand_play_offensive_start(self): '''returns a play_start play for offensive player x = -field_width to +field_width y = -0.5 to -self.max_setback''' a = self.rand_position_width() b = self.rand_setback() c = 0 return ['play_start', a, b, c] def rand_play_set(self): '''returns a list of plays (a play_set) always begins with a 'play_start' type play [['play_start', a, b, c], ['play_random', a, b, c], ...''' num_plays = random.randrange(1, self.max_plays_in_play_set + 1) play_set = [] play_set.append(self.rand_play_offensive_start()) for _ in range(1, num_plays): play_set.append(self.rand_play()) return play_set def rand_roster(self, team_size=2): '''Loads a full roster into self.roster offense = team_size, with a matching default defense''' self.roster = [] for _ in range(team_size): p = self.player_stats(offense=True, play_set=self.rand_play_set(), move_history=[]) self.roster.append(p) self.set_defense() if not self.check_formatting(): raise Exception('FORMATTING ERROR: rand_roster()') def return_scrimmage(self, rate, scrimmage_options): '''returns a scrimmage instance if rate=True, rates the scrimmage (False for gif_it) scrimmage_options are passed in by MutaGenetics control''' scrimmage = Scrimmage(**scrimmage_options) scrimmage.load_roster(self.roster) scrimmage.hist_mutations = self.hist_mutations scrimmage.run_scrimmage(rate) if rate: print 'Scrimmage Rating: %d' % scrimmage.rating return scrimmage def select_offense(self): '''returns player, player_list of all offensive players helper function for change_xxx functions''' offensive_players = [p for p in self.roster if p.offense == True] player = random.choice(offensive_players) offensive_players.remove(player) return player, offensive_players def change_start(self): '''helper for change_offensive_play changes starting position of random offensive player''' player, remainder = self.select_offense() player = self.player_stats(offense=True, play_set=[self.rand_play_offensive_start()] + player.play_set[1:], move_history=[]) return [player] + remainder def change_play_delete(self): '''helper for change_offensive_play deletes a non 'play_start' play from random offensive player''' player, remainder = self.select_offense() starting_play = player.play_set[0] play_list = player.play_set[1:] if len(play_list): play_list.remove(random.choice(play_list)) player = self.player_stats(offense=True, play_set=[starting_play] + play_list, move_history=[]) return [player] + remainder def change_play_add(self): '''helper for change_offensive_play adds a random play to a random offensive player''' player, remainder = self.select_offense() i = random.randint(1, len(player.play_set)) play_list = player.play_set[:i] + [self.rand_play()] + player.play_set[i:] player = self.player_stats(offense=True, play_set=play_list, move_history=[]) return [player] + remainder def change_abc(self): '''helper for change_offensive_play changes random {a, b, c}, one value, for one play, for one random offensive player''' player, remainder = self.select_offense() if len(player.play_set) > 1: i = random.randint(1, len(player.play_set) - 1) if i != 0: play_list = player.play_set[:] j = random.randint(1,3) if j < 3: play_list[i][j] = random.random() else: play_list[i][j] = random.randint(0,self.max_play_duration) player = self.player_stats(offense=True, play_set=play_list, move_history=[]) return [player] + remainder def change_offensive_play(self): '''Random change to single random offensive player is made, change selected from mutation_types at random''' mutation_types = [self.change_abc, self.change_play_add, self.change_play_delete, self.change_start, ] change = random.choice(mutation_types) players = change() self.roster = players self.set_defense() self.hist_mutations.append(change.__name__) if not self.check_formatting(): raise Exception() def shuffle(self): '''Shuffles the plays in a single offensive players play_set play_set[0] + play_set[r] + play_set[r}... no plays added, deleted, or changed, order shuffled only''' player, remainder = self.select_offense() start = player.play_set[0] plays = player.play_set[1:] random.shuffle(plays) player = self.player_stats(offense=True, play_set=[start] + plays, move_history=[]) self.roster = [player] + remainder self.set_defense() self.hist_mutations.append('shuffle') if not self.check_formatting(): raise Exception() def mutate(self, roster2): '''New playset random player equals player[head] + other_player[tail] splicing head to tail of two random players, replacing this new player for the player[head] player''' player_1, remainder = self.select_offense() player_2, _ = self.select_offense() plays_1 = player_1.play_set plays_2 = player_2.play_set play = plays_1[0] plays_1 = plays_1[1:] plays_2 = plays_2[1:] if len(plays_1) > 0: i = random.randint(0, len(plays_1)) plays_1 = plays_1[:i] if len(plays_2) > 0: i = random.randint(0, len(plays_2)) plays_2 = plays_1[i:] player_1 = self.player_stats(offense=True, play_set=[play] + plays_1 + plays_2, move_history=[]) self.roster = [player_1] + remainder self.set_defense() self.hist_mutations.append('mutate') def check_sides_equal(self): '''Are there equal numbers of offensive and defensive players?''' num_off = len([p for p in self.roster if p.offense == True]) num_def = len([p for p in self.roster if p.offense == False]) return num_off == num_def def check_start_first(self): '''Does each play_set start with a 'play_start' play"''' return all([p.play_set[0][0] == 'play_start' for p in self.roster]) def check_defenders_line_up(self): '''Does each offensive player have a corresponding defender? are the starting x's equal''' off_x = [p.play_set[0][1] for p in self.roster if p.offense == True] def_x = [p.play_set[0][1] for p in self.roster if p.offense == False] return all([x in def_x for x in off_x]) def check_range_abc(self): '''Are a & b between 0.0 and 1.0? note 100 is the default length for defensive_pass_coverage''' ab = [] c = [] for p_stat in self.roster: for plays in p_stat[1][1:]: ab.append(plays[1]) ab.append(plays[2]) c.append(plays[3]) ab = all([(0.0 <= abs(f) <= 1.0) for f in ab]) c = all([(i >= 0 and i <= self.max_play_duration) or i == 100 for i in c]) return (ab and c) def check_formatting(self): '''returns True if Roster.roster meets all formatting checks Is this a well formatted Roster.roster?''' formatting_checks = (self.check_sides_equal() and self.check_start_first() and self.check_defenders_line_up() and self.check_range_abc() ) return formatting_checks class MutaGenetics(): '''Holder for a single Evolution Instance MutaGenetics manages the Roster, which loads the Players into a Scrimmage''' test_run = namedtuple('test_run', 'rating roster hist_mutations') def __init__(self, team=2, keepers=2, cycles=2): self.num_team = team self.num_cycles = cycles self.num_keepers = keepers self.pool = [] #this is the pool, the muck as it were, from which all evolve self.scrimmage_options = {'max_turns':25, 'define_open':2.5, 'field_width':20, 'field_backfield':5, 'field_front':15, 'check_proximity':True, 'max_turns_always':False} self.num_tr_random = 0 def __repr__(self): t = '\nMutaGenetics: Evolution Cycle:\n' t += '\tNumber on Team: %d\n' % self.num_team t += '\tNumber of Keepers: %d \t(is the pool size)\n' % self.num_keepers t += '\tNumber of Cycles: %d\n' % self.num_cycles for tr in self.pool[:self.num_keepers]: t += '\t\t%s\n' % str(tr) return t def pool_cull(self): '''Culls self.pool down to num_keepers of best test_runs criteria in order: rating of roster total number of offensive plays in roster length of mutation history''' #This creates a list of tuples (rating, num_plays, len(mutations), test_run) primordial_ooze = [] for tr in self.pool: offensive_players = [p for p in tr.roster if p.offense == True] num_offensive_plays = sum([len(p.play_set) for p in offensive_players]) num_mutations = len(tr.hist_mutations) primordial_ooze.append((tr.rating, - num_offensive_plays, - num_mutations, tr)) #Having a list of tuples, this sorts the test_runs by criteria primordial_ooze.sort(reverse=True) rating_groups = [(rating, list(num_num_tr)) for rating, num_num_tr in groupby(primordial_ooze, lambda x: x[0])] rating_groups.sort(reverse=True) best_in_group = [b[0] for _, b in rating_groups] #And finally, the best test_runs are laoded back into self.pool best_test_runs = [tr for _, __, ___, tr in best_in_group] self.pool = best_test_runs[:self.num_keepers] def tr_random(self): '''Returns a new random test_run. test_run = (rating=float, roster=[player_stat(offense, play_set, move_history), player_stat, ect, ...], hist_mutations=['rand_roster'])''' roster = Roster(roster=[], hist_mutations=['rand_roster']) roster.rand_roster(self.num_team) scrimmage = roster.return_scrimmage(rate=True, scrimmage_options=self.scrimmage_options) tr = self.test_run(float(scrimmage.rating), roster.roster[:], scrimmage.hist_mutations[:]) self.num_tr_random += 1 return tr def tr_mutate_play(self, tr): '''Random simple play change is made on passed test_run, add_play, delete_play, rand_abc, and so on, from Roster()''' tr = deepcopy(tr) roster = Roster(tr.roster, tr.hist_mutations) roster.change_offensive_play() scrimmage = roster.return_scrimmage(rate=True, scrimmage_options=self.scrimmage_options) tr = self.test_run(float(scrimmage.rating), roster.roster[:], scrimmage.hist_mutations[:]) return tr def tr_mutate_cross(self, tr1, tr2): '''Cross Pollination Mustation of tr1 & tr2. Returns random slice of tr1[start] + tr2[end]. 0+ play_set.plays dropped from end of tr1. 1+ play_set.plays dropped from start of tr2''' tr1 = deepcopy(tr1) tr2 = deepcopy(tr2) roster = Roster(tr1.roster, tr1.hist_mutations) roster.mutate(tr2) scrimmage = roster.return_scrimmage(rate=True, scrimmage_options=self.scrimmage_options) tr = self.test_run(float(scrimmage.rating), roster.roster[:], scrimmage.hist_mutations[:]) return tr def tr_mutate_shuffle(self, tr): '''Test_run is returned with a single players play_set shuffled. play_set[0] + random slice and order of play_[remainder]''' tr = deepcopy(tr) roster = Roster(tr.roster, tr.hist_mutations) roster.shuffle() scrimmage = roster.return_scrimmage(rate=True, scrimmage_options=self.scrimmage_options) tr = self.test_run(float(scrimmage.rating), roster.roster[:], scrimmage.hist_mutations[:]) return tr def gif_pool(self, sN, n=0): '''Creates a gif of pool.best[n] in ./gifs''' tr = self.pool[n] roster = Roster(tr.roster, tr.hist_mutations) scrimmage = roster.return_scrimmage(rate=False, scrimmage_options=self.scrimmage_options) title = '\nSCORE: %d\nHISTORY:\n\t' % tr.rating title += '\n\t'.join(tr.hist_mutations) title += '\n\n' print title, sN sN = './gifs/%s_%d_%d_r%d.gif' % (sN, self.num_keepers, self.num_cycles, tr.rating) scrimmage.gif_it(sN, title) def basic_evolve(self, test=False): '''Starting from random rosters, seeks to find optimal roster, given the hardwired rating criteria in Scrimmage We're calling it basic, because the next step is to analyse this function and try to improve on it For Reference: test_run = namedtuple('test_run', 'rating roster hist_mutations')''' #Initial Pool of Random Rosters while len(self.pool) < self.num_keepers: tr = self.tr_random() #print tr.rating if tr.rating > 0.0 or test: self.pool.append(tr) print 'SAVED: %d' % tr.rating #Cycles -- can be zero for _ in range(self.num_cycles): #One per keeper (per cycle) each of the major MutaGenetic types for i in range(self.num_keepers): self.pool.append(self.tr_random()) self.pool.append(self.tr_mutate_play(self.pool[i])) self.pool.append(self.tr_mutate_cross(self.pool[i], random.choice(self.pool))) self.pool.append(self.tr_mutate_shuffle(self.pool[i])) #Cull it Back Down to self.num_keepers self.pool_cull() def report(self, run_num): self.pool_cull() #test_run = namedtuple('test_run', 'rating roster hist_mutations') with open('./reports/MutaGenetics.txt', 'a') as f: for tr in self.pool: offensive_play_sets = [p.play_set for p in tr.roster if p.offense == True] offensive_play_list = [item for sublist in offensive_play_sets for item in sublist] #flattens play_names = [p[0] for p in offensive_play_list] print offensive_play_list t = '%d, ' % run_num t += '%d, ' % tr.rating t += '%s, ' % ' '.join(play_names) t += '%s, ' % ' '.join(tr.hist_mutations) t += '%d' % self.num_tr_random t += '\n' f.write(t) def create_dirs(): '''Creates (and clears if exists) ./temp & ./gifs directories''' dir_temp = './temp' dir_gifs = './gifs' dir_reports = './reports' file_report = './reports/MutaGenetics.txt' header_report = 'run,rating,plays,mutations,random\n' for d in [dir_temp, dir_gifs, dir_reports]: if not os.path.exists(d): os.makedirs(d) if not os.path.exists(file_report): with open(file_report, 'w') as f: f.write(header_report) if __name__ == '__main__': create_dirs() for r in range(10): mG = MutaGenetics(team=2, keepers=10, cycles=25) mG.basic_evolve(test=False) mG.gif_pool('25_report_best_%d_' % r, 0) mG.report(r) #print mG print "FINISHED: FOOTBALL" ''' TODO: Analysis in seperate module Analysis of Mutations Used Analysis of Plays Used Defenders Better (maybe more than one defense type) Ratings, change to sortable String 'pass:1 center:45 ... etc' Maybe with comma seperated for ease of computation Done '''