''' Created on Jul 2, 2015 Copyright Brett Paufler @author: Brett Paufler Start with football_tournament, unless refining plays, football_tournament runs the show football_tournament is the main outward facing module. football_tournament calls scrimmage scrimmage calls player This is most definitely a work in progress. Need to improve the logic of the individual play routines (block, throw, etc.) But this remains a good basis refactored 9-4-15 TODO: play logic improvement run, block, cover, add more, etc This is the major todo centered_matrix may help with the logic Remember + = offense - = defense ''' import football_roster import football_field from football_matrix import centered_matrix from skimage.io import imsave import numpy as np import math from utilities import gif_it, reduce_img_list from copy import deepcopy from itertools import product from operator import __lt__, __gt__ class Player(): '''Players are called by Scrimmage. Player is a runtime implementation of a player (a nested list) from a football_roster: football_roster = [11 players, 6 plays each, 4 parts to each play].''' def __init__(self, record): '''Players are called and managed by scrimmages. Play Logic is contained in the scrimmage class. record is a single nested list football_roster 11 players to a football_roster player implements one player. record = raw nested list input stream = current [['run', 000.000, 999.999, 1.0], ['block, etc... number = player number positive numbers mean an offensive player negative, defensive x, y = current x,y coordinates of player relative to ball start vx, vy = velocity in x,y coordinates dx, dy = delta, current proposed change to current velocity z = is rotation, facing of player (poorly implemented at this point) ''' self.record = record #raw nested list in self.stream = deepcopy(record[2:]) self.number = record[0][3] self.x = record[1][1] self.y = record[1][2] self.vx = 0.0 self.vy = 0.0 #v is current value self.dx = 0.0 #delta's, proposed new self.dy = 0.0 #THESE ARE PERHAPS VERY FRAGILE at this point #Straight conversion to ints #self.stats = self.abc_int(record[:25]) self.max_vel = 3.0 #10yards/sec = 30'/sec * sec/10turn reasonable speed = 3'/0.1s self.max_delta = 1.0 #Somewhere Between 1&2, 3y/s/s on the up side 9/4, call it two self.holding_player = record[0][0] #holding, does nothing self.holding_value_1 = record[0][1] #holding, does nothing self.holding_value_2 = record[0][2] #holding, does nothing self.blocking_distance = 2.5 #magic number for now 2.5' self.hit_val = 100.0 #hard coded for now #Determines Rotation (z) by which side player starts on #0 = up, 90 = right, 180 = down, 270 = left if self.y < 0: self.z = 0.0 #offense starts up, 0 else: self.z = 180.0 #defense hard coded to start 180, down #Check for Ball could be better, hard wired to center if self.record[2][0] == 'snap' and self.number == 0: self.has_ball = True else: self.has_ball = False def __repr__(self): t = 'Player: {: >2}\n'.format(self.number) t += '\trecord: {}\n'.format(self.record) t += '\tstream: {}\n'.format(self.stream) t += '\tmax_vel: {}, max_delta: {}, hit_val: {}'.format(self.max_vel, self.max_delta, self.hit_val) t += '\tPosition: x:%.2f, y:%.2f,, z:%.2f\n' % (self.x, self.y, self.z) t += '\tVelocity: %.2f, %.2f\n' % (self.vx, self.vy) t += '\tDelta: %.2f, %.2f\n' % (self.dx, self.dy) t += '\tHas_Ball: %s\n' % str(self.has_ball) return t def stream_advance(self): '''Decremits current stream counter by one and advances to next play if <= 0. Test is performed after play, so if 1, play still fires. A value of 999 is infinite.''' if len(self.stream) != 1 and self.stream[0][3] != 999: self.stream[0][3] -= 1 if self.stream[0][3] <= 0: self.stream = self.stream[1:] def distance(self, b): '''Returns the absolute distance between two players''' return np.linalg.norm(np.array([self.x, self.y]) - np.array([b.x, b.y])) def norm(self, x, y, max_val): '''Normalizes two values (x,y), such that the distance (0,0) to (x,y) is less than max_val, while keeping both values proportionally the same.''' xy = math.sqrt(pow(x,2) + pow(y,2)) if xy > max_val: x = x / xy * max_val y = y / xy * max_val return (x, y) def delta_cascade(self): '''Player's delta applied to velocity, which in turn is applied to position. Or in long form: dx, dy is normalized dx, dy applied to vx, vy vx, vy is normalized vx, vy applied to x, y dx, dy zeroed out for next turn.''' self.dx, self.dy = self.norm(self.dx, self.dy, self.max_delta) self.vx, self.vy = (self.dx + self.vx), (self.dy + self.vy) self.vx, self.vy = self.norm(self.vx, self.vy, self.max_vel) self.x += self.vx self.y += self.vy self.dx = 0.0 self.dy = 0.0 class Scrimmage(): '''Manages a 'Down', otherwise known as a scrimmage. football_tournament --> scrimmage --> player All scrimmage/play logic (play implementation) to be encapsulated in scrimmage. But the origination of the plays come from football_tournament and football_roster. A scrimmage turn is +/- 0.1 second long. A typical real life scrimmage lasts 3-30 turns.''' def __init__(self, offensive_roster=None, defensive_roster=None, field=football_field.HalfField()): '''If no rosters are passed, random ones will be created, but unless testing, this will likely lead to meaningless results. See self.test_all_prebuilt_plays for football_roster options. scrimmage typically called by football_tournament.''' self.load_rosters(offensive_roster, defensive_roster) #sets self.players, self.img_list self.field = field #Yes, anothing thing that needs some TLC def load_rosters(self, offensive_roster, defensive_roster): '''Loads rosters each being a (11, 6, 4) nested list.''' if not offensive_roster: offensive_roster = football_roster.offense() football_roster.valid(offensive_roster, offense=True) if not defensive_roster: defensive_roster = football_roster.defense(offensive_roster) football_roster.valid(defensive_roster, offense=False) self.players = sorted([Player(p) for p in offensive_roster + defensive_roster], key = lambda x: x.number, reverse=True) self.img_list = [] def turn(self): '''Runs a turn (0.1s) of the scrimmage. i.e. runs next play in stream, advances stream, then delta cascade.''' for player in self.players: getattr(self, player.stream[0][0])(player) #runs first play in stream player.stream_advance() for player in self.players: player.delta_cascade() def run_scrimmage(self, num_turns, name='test_run_no_name', images=True, gif=True): '''Runs the scrimmage saving images and gif's as appropriate. Outside of the self.turn() part of the loop, this is all image control (saving png's and gif's.''' for d in range(0, num_turns): if d != 0: #first turn skip, yields turn=0 pre-hike position self.turn() if images or gif: img = self.field.img_to_list(self, turn=d) self.img_list.append(img) if images: sN='./images/%s_turn_%d.png' % (name, d) print sN imsave(sN, img) if gif: for img in self.img_list: img[299, 10:330, 1] = 255 #Hack to Keep Bottom Green Line gif_it(reduce_img_list(self.img_list, 0.20), './images/%s_gif.gif' % name) def player_by_number(self, n): '''Returns the player in self.players with player.number == n.''' #Throws error if no such player. return [p for p in self.players if p.number == n][0] def team(self, player, mates=True): '''Returns list of all other players on same team if mates=True or opponents if mates=False. So, team mates=True is equivalent to teamates.''' if ((player.number > 0 and mates == True) or (player.number < 0 and mates == False)): oper = __gt__ else: oper = __lt__ return [p for p in self.players if oper(p.number, 0.0) and p != player] #TODO - This is a work in progress #Makes a Matrix, but what to do with it #SO, not really working = in progress def matrix(self, center_on=None, radius=15):#player, size=, vision=()): '''Centered on Ball, half_size''' if not center_on: x, y = 0, 0 elif isinstance(center_on, tuple): #, class_or_type_or_tuple)center_on == None: x, y = center_on[0], center_on[1] else: x, y = center_on.x, center_on.y print 'center_on:\t', center_on print x, y m = centered_matrix(radius, center=(x,y)) for p in self.players: m.load_line((p.x, p.y), (p.x + p.vx + p.dx, p.y + p.vy + p.dy), p.number) np.set_printoptions(linewidth=250) print m def distance(self, x1, y1, x2, y2): '''Returns Eucledian distance between two points (x1, y1) & (x2, y2).''' return np.linalg.norm(np.array([x1,y1]) - np.array([x2,y2])) def player_distance_tuple_list(self, player, mates): '''Returns a list of (distance, player) tuples: [(distance, player), (distance, player), etc. if mates == True, for teammates else, opposition''' return [(self.distance(player.x, player.y, p.x, p.y), p) for p in self.team(player, mates=mates)] def closest_player(self, player, mates=True, return_distance=False, closest=True): '''Returns closest player by p.x, p.y to the passed player. If mates=True, looking for closest teammate, otherwise, searches opposition. Default returns a player instance, but if return_distance is True, returns a tuple (distance, player). Analysis is NOW based (dx, dy ONLY), which means vx & dx are not considered. if closest=False, returns furthest player instead.''' p_list = sorted(self.player_distance_tuple_list(player, mates=mates)) if not closest: p_list = list(reversed(p_list)) #for furthest p = p_list[0] if return_distance: return p else: return p[1] # PLAYS # FROM HERE DOWN KEYWORDS IMPLEMENTATION # FOR ROSTER PLAYS def snap(self, player): '''Play: ''' #Takes maybe a half second, so at one turn, it's fast player.has_ball = False _ = [p for p in self.players if p.number == 1][0].has_ball = True #To QB, such a hack def throw(self, player): '''Play: ''' pass #NOT IMPLEMENTED, OBVIOUSLY #Has ball, taken Care of by snap and throw... (perhaps movment modified) def catch(self, player): pass def closest(self, player): '''Play: player tracks (follows) the closest opposition player (i.e. the quarry). Opposition player can change from turn to turn. Selection is p.x, p.y based (no provision for p.vx or p.dx, i.e. no delta).''' _, q = self.closest_player(player, mates=False, return_distance=True) #distance doesn't matter player.dx += q.x + q.vx + q.dx - player.x player.dy += q.y + q.vy + q.dy - player.y def cover(self, player): '''Play: ''' pass #NOT IMPLEMENTED, OBVIOUSLY def run(self, player): '''Play: player runs in direction given (full bore to the best of their ability).''' a, b = player.norm(player.stream[0][1], player.stream[0][2], player.max_delta) player.dx += a player.dy += b def block(self, player): '''Play: player performs a very simplistic, unidirectional, block. If any player on opposition is within range (and lower on the field) opposition player's v and d values set to zero Very Weak''' #TODO - implement so it's generalize (so offense can use, works any direction) for p in self.team(player, mates=False): if (abs(p.x - player.x) < 2.5 and player.y > p.y and player.y - p.y < 2.5): p.vx, p.vy, p.dx, p.dy = 0.0, 0.0, 0.0, 0.0 def test(self): '''Tests the scrimmage class. Called in test_all_prebuilt_plays(), next.''' #Hardwired for players to be numbered 1 to 11, -1 to -11 for n in range(1, 12) + range(-1, -12, -1): p = self.player_by_number(n) assert n == p.number assert 10 == len(self.team(player=p, mates=True)) assert 11 == len(self.team(player=p, mates=False)) def test_all_prebuilt_plays(self, num_turns=5, images=True, gif=True): '''Runs test() and all off start/play against all def start/play.''' #default tests _ = [self.test() for _ in range(num_turns)] match_ups = product(football_roster.list_all('off_start'), football_roster.list_all('off_play'), football_roster.list_all('def_start'), football_roster.list_all('def_play') ) for off_start, off_play, def_start, def_play in match_ups: print off_start, off_play, def_start, def_play offensive_roster = football_roster.offense(start=off_start, play=off_play) defensive_roster = football_roster.defense(offensive_roster, start=def_start, play=def_play) self.load_rosters(offensive_roster, defensive_roster) self.run_scrimmage(num_turns=num_turns, name=off_start[10:] + off_play[8:] + '_vs_' + def_start[10:] + def_play[8:], images=images, gif=gif) if __name__ == '__main__': print 'Scrimmage Called Direct!\n\tIf not testing, might You Want football_tournament, instead?\n' offense = football_roster.offense(start='off_start_center_line', play='off_play_all_run') defense = football_roster.defense(offense, play='def_play_all_track_closest') #football_roster.pretty_print(defense) s = Scrimmage(offensive_roster=offense, defensive_roster=defense) #s.run_scrimmage(num_turns=5, name='all_run', images=True, gif=True) #s.test() s.test_all_prebuilt_plays(num_turns=10, images=False, gif=True) print 'SCRIMMAGE OVER'