''' Created on Jun 27, 2015 @author: Brett Paufler (c) Copyright Brett Paufler First major refactor: Aug 6th, 2015. Second major refactor Sep 3rd, 2015. A roster is a nested list of nested lists of fixed shape 11, 6, 4 (11 players, 11 plays, 4 parts to each play) which is used to direct the logic of a football football_scrimmage (one roster each for offense and defense) sample_roster = [[[str, float, float, float], [str, float, float, float], ... [['player', 000.000, 000.000, 0.000], ['start', -78.00, -0.50, 0.000], ... [... ... ] Roster was previously implemented as a class, but since none of the functions used 'self', a class didn't seem appropriate. Funtions that are suitable for exporting to other modules include: twiddle - slightly alters an existing roster weave - interweaves two rosters together taking random bits from each offense - provide an offensive roster (with custom start and play abilities) defense - provide a defensive roster (typically in response to a passed offensive roster) ''' from random import choice, uniform from itertools import compress from copy import deepcopy #List of Valid Plays plays = ['catch', 'block', 'run', 'cover', 'closest'] #snap: hardwired for center only at this point #throw: hardwired for quarterback only at this point def rand(start=-999, finish=999, digits=3): '''Returns a well formated random float, from 000.000 to 999.999''' return round(uniform(start, finish), digits) def random_roster(): '''Returns a random offensive roster, as a starting template on which to build more complicated rosters.''' r = [] for p in range(1, 12): p = [['player', 000.000, 000.000, float(p)], ['start', rand(-78.0,78.0), rand(-0.5, -10.0), 0.0] ] for _ in range(4): p.append([choice(plays), rand(), rand(), rand(1.0, 25.0, digits=0) ]) r.append(p) #Center and Quarterback: hardwired plays r[0][1:4] = [['start', 0.0, -0.5, 0.0], ['snap', 0.0, 0.0, 1.0], ['block', 0.0, 0.0, 999.0]] r[1][1:4] = [['start', 0.0, -1.5, 0.0], ['catch', 0.0, 0.0, 1.0], ['throw', 0.0, 0.0, 999.0]] return r def pretty_print(roster): '''Pretty prints the roster's nested loops. Use stringify if a text string is desired.''' t = 'Raster: [\n' for r in roster: t += '\t %s,\n' % (str(r)) t += ' ]' print t def stringify(roster): '''Returns a string version of the roster's nested lists, suitable for printing or saving to file.''' s = '' for r in roster: for a, b, c, d in r: s += '{: <10} {:8.3f} {:8.3f} {:8.3f}\t'.format(a, b, c, d) s += '\n' s = s[:-1] #slice off trailing '\n' return s def save(roster, sN='offense/test_no_name.txt'): '''Saves passed roster to file''' with open(sN, 'w') as f: f.write(stringify(roster)) def load(roster_text): '''Converts a properly formatted text string to a roster.''' roster = [] for player_string in roster_text.split('\n'): p = player_string.split() player = [] for i in range(0, len(p), 4): player.append([p[i], float(p[i+1]), float(p[i+2]), float(p[i+3])]) roster.append(player) valid(roster) return roster def load_file(fN): '''Loads a saved roster from file.''' with open(fN, 'r') as f: raw_string = f.read() return load(raw_string) def ijk(i_range=None, j_range=None, k_range=None): '''Returns a tuple that indexes a roster: returns a tuple (i, j, k) to be used as an index to a roster[i][j][k]. i_range is 0 - 10 by default: player j_range is 0 - 5 by default: play k_range is 0 - 3 by default: part''' if i_range == None: i_range = range(0,11) if j_range == None: j_range=range(0,6) if k_range == None: k_range=range(0,4) i = choice(i_range) j = choice(j_range) k = choice(k_range) return (i, j, k) def twiddle_play(roster): '''Replaces random existing play with random new play. Existing and new may be the same play, so no change is possible.''' i, j ,_ = ijk(j_range=range(2,6)) #if first or second player, third play, offense roster: protects QB snap/catch if (i == 0 or i == 1) and j == 2 and roster[1][0][3] >= 0: return roster else: roster[i][j][0] = choice(plays) return roster def twiddle_x(roster): '''Randomly changes some x value (first float) in the roster. Note: 'player' and 'start' play values are protected. ['some_play', x_float, y_float, turn_float]''' i, j, _ = ijk(j_range=range(2,6)) roster[i][j][1] = rand(-999.999, +999.009, digits=3) return roster def twiddle_y(roster): '''Randomly changes some y value (second float) in the roster. Note: 'player' and 'start' play values are protected. ['some_play', x_float, y_float, turn_float]''' i, j, _ = ijk(j_range=range(2,6)) roster[i][j][2] = rand(-999.999, +999.009, digits=3) return roster def twiddle_i(roster): '''Randomly changes some i value (thrid float == turn_float) in the roster. Note: 'player' and 'start' play values are protected. ['some_play', x_float, y_float, turn_float]. As with all twiddles, sometimes there is no change.''' roster = deepcopy(roster) i, j ,_ = ijk(j_range=range(2,6), k_range=[3]) #if first or second player, third play, offense roster: protects QB snap/catch if (i == 0 or i == 1) and j == 2 and roster[1][0][3] >= 0: return roster else: roster[i][j][3] = rand(0.000, 25.000, digits=0) return roster def twiddle_start(roster): #TODO - not implemented yet return roster def twiddle(roster, num=1, t_play=True, t_x=True, t_y=True, t_i=True, t_start=False): '''Returns a twiddled roster randomly altered using one of the enabled options, selected randomly: roster = 11 players 6 plays each, each play ['play', 'flaot', 'float', 'float'] [t_play, t_x, t_y, t_i] num = number of changes to make/attempt (sometimes old == new) t_play: whether the play type CAN change if False, play type never changes t_x: whether the x value of a play can change if False, play type never changes t_y: whether the y value of a play can change if False, play type never changes t_i: whether the i value of a play can change (also known as the turn value) if False, play type never changes t_start: whether the starting position of the players can change if False, starting postion never changes As of 9-3-15, this is not implemented''' #Compress ~ Filter: e.g. if t_x is true, twiddle_x is added to list, otherwise, no filtered_twiddle_choices = list(compress([twiddle_play, twiddle_x, twiddle_y, twiddle_i, twiddle_start], [t_play, t_x, t_y, t_i, t_start])) for _ in range(num): twiddle_this = choice(filtered_twiddle_choices) roster = twiddle_this(roster) return roster def weave(roster_a, roster_b, ratio=(1,1), keep_head=2): '''Randomly merges the components of two rosters together. The two ratio values determine liklihood component will come from each roster 1,1 denotes and equal chance component comes from each 2:1 means components of first roster will be returned twice as often as components from second roster 0:1 means only the second roster will be used keep_head=2, preserves the player and start information roster_a, otherwise this too is selected proportionally''' a_b = [roster_a] * ratio[0] + [roster_b] * ratio[1] c = deepcopy(roster_a) for i in range(11): for j in range(6): if j < keep_head: c[i][j] = roster_a[i][j] else: c[i][j] = choice(a_b)[i][j] return deepcopy(c) def off_play_all_run(roster): '''offense() helper function, sets all players to run''' roster[0][3] = ['run', 0.0, 999.999, 999.0] roster[1][3] = ['run', 0.0, 999.999, 999.0] for p in range(2, 11): roster[p][2] = ['run', 0.0, 999.999, 999.0] return roster def off_play_all_fan_out(roster): '''offense() helper function, sets all players running at 45 degrees from center.''' roster[0][3] = ['run', 0.0, 999.999, 999.0] roster[1][3] = ['run', 0.0, 999.999, 999.0] for p in range(2, 11): if roster[p][1][1] == 0.0: roster[p][2] = ['run', 0.0, 999.999, 999.0] elif roster[p][1][1] < 0.0: roster[p][2] = ['run', -999.999, 999.999, 999.0] else: roster[p][2] = ['run', 999.999, 999.999, 999.0] return roster def off_start_center_line(roster): '''offense() helper, flat center line up''' players = range(2,11) position = [-2.0, 2.0, -4.0, 4.0, -6.0, 6.0, -8.0, 8.0, -10.0] #last is a stringer for p, x in zip(players, position): roster[p][1] = ['start', x, -0.5, 0.0] return roster def off_start_center_wedge(roster): '''offense() helper, wedge shape line up''' players = range(2,11) x_pos = [-2.0, 2.0, -4.0, 4.0, -6.0, 6.0, -8.0, 8.0, -10.0] #last is a stringer y_pos = [-abs(x/2.0) for x in x_pos] for p, x, y in zip(players, x_pos, y_pos): roster[p][1] = ['start', x, y, 0.0] return roster def def_start_match_offense(offense): '''defense() helper, returned roster's start is mirror image of passed offense roster (start y = -y).''' defense = deepcopy(offense) for i in range(11): defense[i][1][2] = - defense[i][1][2] return defense def def_play_all_stand_still(roster): '''defense() helper, Sets all players in (defensive) roster to 'run' nowhere forever.''' for p in range(0, 11): roster[p][2] = ['run', 0.0, 0.0, 999.0] return roster def def_play_all_basic_block(roster): '''defense() helper, Sets all players in (defensive) roster to 'block' for 999 turns''' for i in range(11): roster[i][2] = ['block', 0.0, 0.0, 999.0] return roster def def_play_all_track_closest(roster): '''defense() helper, Sets all players in (defensive) roster to 'closest' for 999 turns. 'closest' causes a player to head towards (track) the closest opposition player each turn. Player tracked can change each turn.''' for i in range(11): roster[i][2] = ['closest', 0.0, 0.0, 999.0] return roster def list_all(start): '''Helper for offense() and defense() Returns a list of all global functions that start with passed phrase. To be called internally and other modules to list possible: off_start off_play def_start def_play''' return [g for g in globals() if str(g).startswith(start)] def offense(start=None, play=None): '''Returns an offensive roster. If no arguments passed, roster selects randomly: start from all functions that startswith('off_start') in this module play from all functions that startswith('off_play') in this module. This is the main facing offensive roster interface.''' roster = random_roster() if not start: start = choice(list_all('off_start')) roster = globals()[start](roster) if not play: play = choice(list_all('off_play')) roster = globals()[play](roster) valid(roster, offense=True) return roster #Note the similiarities to offense above def defense(offense, start=None, play=None): '''Returns a defensive roster. if offense=None, defense inputs a random offensive roster If no arguments passed, roster selects random: start from all startswith('off_start') in this module play from all startswith('off_play') in this module. Main facing offensive roster interface.''' if not start: start = choice(list_all('def_start')) roster = globals()[start](offense) if not play: play = choice(list_all('def_play')) roster = globals()[play](roster) for p in range(0, 11): #defensive players are negatively numbered roster[p][0][3] = - roster[p][0][3] valid(roster, offense=False) return roster def valid(roster, offense=True): '''Raises an assertion exception if roster is not well formatted.''' if offense: #Hardwired Offensive Plays assert roster[0][2][0] == 'snap' assert roster[1][2][0] == 'catch' for r in roster: assert r[0][0] == 'player' assert r[1][0] == 'start' if offense: #Off-Sides Check (checking starting y not offsides) assert r[1][2] < 0 else: assert r[1][2] > 0 assert r[1][1] <= 80.0 #Start Onfield: insure starting x is in bounds assert r[1][1] >= -80.0 for p in r: #Check Proper Sequencing: [str, float, float, float] assert isinstance(p[0], str) assert isinstance(p[1], float) assert isinstance(p[2], float) assert isinstance(p[3], float) def test_roster_module(verbose=False, num=10): '''Test for Validity of featured functions in the Roster class If this test ever throws and error, it's the maintainer's fault.''' print 'RUNNING TEST: ROSTER()' for n in range(1, num + 1): print 'TEST LOOP %d of %d' % (n, num) roster_1 = twiddle(offense(), num=n*10, t_play=True, t_x=True, t_y=True, t_i=True, t_start=True) valid(roster_1) roster_2 = twiddle(offense(), num=n*10, t_play=True, t_x=True, t_y=True, t_i=True, t_start=True) valid(roster_2) for _ in range(num): roster_1 = weave(roster_1, roster_2, ratio=(1,n), keep_head=0) roster_2 = weave(roster_2, roster_1, ratio=(n,1), keep_head=2) valid(roster_1) valid(roster_2) save(roster_1, 'offense/save_test.txt') valid(load_file('offense/save_test.txt')) for _ in range(num): off = offense() valid(off, offense=True) valid(defense(off), offense=False) print 'ALL TESTS PASSED: MUTAGENTICS PART WORKING' if __name__ == '__main__': print 'Running Roster as main' test_roster_module(verbose=False, num=5) off = offense(start='off_start_center_line', play='off_play_all_run') d = defense(off, start=None, play='def_play_all_stand_still')# all_fan off_play_all_fan_out')) off = twiddle(off, num=10, t_play=False, t_x=True, t_y=True, t_i=False, t_start=False) #twiddle(off, num=1000) valid(off) pretty_print(off) ''' TODO - AFter changing to Float, this could stand some serious refactoring Also, twiddle, valid, etc, needs to be refactored again ''' #off = offense() #display(off) #d = defense(off) #display(d)