Advertisement
Shadowfury333

Fix for cumulative playtime

Jun 25th, 2017
195
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.93 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. # Copyright 2017 DBrickShaw
  4.  
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  6. # and associated documentation files (the "Software"), to deal in the Software without restriction,
  7. # including without limitation the rights to use, copy, modify, merge, publish, distribute,
  8. # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
  9. # furnished to do so, subject to the following conditions:
  10.  
  11. # The above copyright notice and this permission notice shall be included in all copies or
  12. # substantial portions of the Software.
  13.  
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
  15. # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  16. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  17. # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  19.  
  20. import os, re, math, datetime, traceback
  21.  
  22. BACKGROUND_COLOR = '000000'
  23. FOREGROUND_COLOR = 'cccccc'
  24. COLOR1 = '00e400'
  25. COLOR2 = '0086b2'
  26.  
  27. COL_GRAD = ['d90000', 'd96c00', 'd9d900', '00d900', '00a3d9', 'bf00ff']
  28.  
  29. TIME_FIELD_WIDTH = 6
  30. STATS_FIELD_WIDTH = 50
  31. STATS_INDENT = 16
  32. HISTOGRAM_TITLE_INDENT = 9
  33. HISTOGRAM_BAR_CHAR = '#'
  34. HISTOGRAM_NUM_ROWS = 18
  35. HISTOGRAM_NUM_COLUMNS = 57
  36.  
  37. class RunStats(object):
  38.     def __init__(self, file):
  39.         self.ending = ''
  40.         self.score = 0
  41.         self.location = ''
  42.         self.power_slots = 0
  43.         self.prop_slots = 0
  44.         self.util_slots = 0
  45.         self.weap_slots = 0
  46.         self.rating = 0
  47.         self.damage = 0
  48.         self.damage_taken = 0
  49.         self.turns_passed = 0
  50.         self.shots_fired = 0
  51.         self.melee = 0
  52.         self.max_alert = 0
  53.         self.low_sec = 0
  54.         self.hacks = 0
  55.         self.success_hacks = 0
  56.         self.actions = 0
  57.         self.visited = 0
  58.         self.play_time = 0
  59.         self.route = []
  60.        
  61.         self.parse(file)
  62.  
  63.     def parse(self, file):
  64.         for line in file:
  65.             line_split = line.split()
  66.            
  67.             if 'Seed:' in line:
  68.                 continue
  69.            
  70.             if '---[' in line:
  71.                 self.ending = line.strip()
  72.            
  73.             # Performance
  74.             if 'TOTAL SCORE:' in line:
  75.                 self.score = int(line_split[-1])
  76.                
  77.             # Cogmind
  78.             if 'Location' in line:
  79.                 self.location = ' '.join(line_split[1:])
  80.            
  81.             # Parts
  82.             if 'Power (' in line:
  83.                 self.power_slots = int(re.sub("[^0-9.]", "", line_split[-1]))
  84.             if 'Propulsion (' in line:
  85.                 self.prop_slots = int(re.sub("[^0-9.]", "", line_split[-1]))
  86.             if 'Utility (' in line:
  87.                 self.util_slots = int(re.sub("[^0-9.]", "", line_split[-1]))
  88.             if 'Weapon (' in line:
  89.                 self.weap_slots = int(re.sub("[^0-9.]", "", line_split[-1]))
  90.                
  91.             # Peak State
  92.             if '[Rating:' in line:
  93.                 self.rating = int(re.sub("[^0-9.]", "", line_split[-1]))
  94.            
  95.             # Stats
  96.             if 'Damage Inflicted' in line:
  97.                 self.damage = int(line_split[-1])
  98.             if 'Damage Taken' in line:
  99.                 self.damage_taken = int(line_split[-1])
  100.             if 'Turns Passed' in line:
  101.                 self.turns_passed = int(line_split[-1])
  102.             if 'Shots Fired' in line:
  103.                 self.shots_fired = int(line_split[-1])
  104.             if 'Melee Attacks' in line:
  105.                 self.melee = int(line_split[-1])
  106.             if 'Maximum Alert Level' in line:
  107.                 self.max_alert = int(line_split[-1])
  108.             if 'Low Security (%)' in line:
  109.                 self.low_sec = int(line_split[-1])
  110.             if 'Total Hacks' in line:
  111.                 self.hacks = int(line_split[-1])
  112.             if 'Successful' in line:
  113.                 self.success_hacks = int(line_split[-1])
  114.             if 'Actions Taken' in line:
  115.                 self.actions = int(line_split[-1])
  116.                
  117.             # Route
  118.             if 'Regions Visited' in line:
  119.                 self.visited = int(line_split[-1])
  120.             if 'Route' in line:
  121.                 route_section = False
  122.                 for line in file:
  123.                     if '-------' in line:
  124.                         route_section = True
  125.                     # KLUDGE - Rating can sometimes appear immediately after
  126.                     #          part with 'Route' in name
  127.                     if '[Rating:' in line:
  128.                         self.rating = int(re.sub("[^0-9.]", "", line_split[-1]))
  129.                     break
  130.                
  131.                 if route_section:
  132.                     for line in file:
  133.                         line_split = line.split()
  134.                         if not line.strip() or 'Game' in line:
  135.                             break
  136.                         if '-------' in line:
  137.                             continue
  138.                         discovered_exits = False
  139.                         discovered_i = 0
  140.                         for i, token in enumerate(line_split):
  141.                             if '(' in token:
  142.                                 discovered_exits = True
  143.                                 discovered_i = i
  144.                                 break
  145.                         if discovered_exits:
  146.                             self.route.append(' '.join(line_split[0:discovered_i]))
  147.                         else:
  148.                             self.route.append(line.strip())
  149.            
  150.             # Game
  151.             if 'Play Time:' in line:
  152.                 self.play_time = int(line_split[-2])
  153.             if 'Cumulative:' in line:
  154.                 self.play_time = int(line_split[-2])                
  155.  
  156.         # Derived Stats
  157.         self.score_per_turn = float(self.score) / self.turns_passed if self.turns_passed > 0 else 0.0
  158.         self.turns_per_min = float(self.turns_passed) / self.play_time if self.play_time > 0 else 0.0
  159.         self.success_hack_perc = (float(self.success_hacks) / self.hacks) * 100 if self.hacks > 0 else 0.0
  160.         self.actions_per_min = float(self.actions) / self.play_time if self.play_time > 0 else 0.0
  161.        
  162.         # DEBUG
  163.         for location in self.route:
  164.             if ('Improved Fusion Compressor' in location or
  165.                 'Experimental Field Recycling Unit' in location or
  166.                 'Sessions:' in location or
  167.                 'Layered Medium Armor Plating' in location or
  168.                 'Improved Utility Shielding' in location or
  169.                 'Experimental Thermal Generator' in location or
  170.                 'Play Time:' in location):
  171.                
  172.                 print 'WARNING: Invalid route data read from morgue file: ' + file.name
  173.                 break
  174.        
  175.         if not self.ending:
  176.             print 'WARNING: Failed to read end condition from morgue file: ' + file.name
  177.      
  178.        
  179.     def slot_config_plaintext(self):
  180.         return ('[' + str(self.power_slots) + '\\' + str(self.prop_slots) + '\\'
  181.                     + str(self.util_slots) + '\\' + str(self.weap_slots) + ']')
  182.                    
  183.     def __str__(self):            
  184.         return  ('{:<6}'.format(self.score) + '    ' + (' '*((STATS_FIELD_WIDTH-len(self.ending))/2)) + self.ending
  185.                 + '\n' + (' '*STATS_INDENT) + '{:<15}'.format(self.slot_config_plaintext())
  186.                     + 'Rating:    ' + ('%3d' % self.rating) + '  ' + ('{:^19}'.format(self.location))
  187.                 + '\n' + (' '*STATS_INDENT) + 'Shots:   ' + ('%4d' % self.shots_fired)
  188.                     + '  Melee:    ' + ('%4d' % self.melee) + '  Visited:         ' + ('%2d' % self.visited)
  189.                 + '\n' + (' '*STATS_INDENT) + 'Damage         Dealt: ' + ('%7d' % self.damage) + '  Taken:      ' + ('%7d' % self.damage_taken)
  190.                 + '\n' + (' '*STATS_INDENT) + 'Max Alert:  ' + str(self.max_alert) + '  Low Sec:  ' + ('%3d' % self.low_sec)
  191.                     + '%' + '  Hacks: ' + ('%3d' %  self.hacks)
  192.                     + ' (' + ('%5.1f' % self.success_hack_perc) + '%)'
  193.                 + '\n' + (' '*STATS_INDENT) + 'Turns: ' + ('%6d' % self.turns_passed) + '  Time:   ' + time_plaintext(self.play_time)
  194.                     + '  Actions:  ' + ('%9d' % self.actions)
  195.                 + '\n' + (' '*STATS_INDENT) + 'SPT: ' + ('%8.2f' % self.score_per_turn) + '  TPM:  ' + ('%8.2f' % self.turns_per_min)
  196.                     + '  APM:  ' + ('%13.2f' % self.actions_per_min))    
  197.  
  198.     def html_str(self, agg_stats):
  199.         return  (cstr('{:<6}'.format(self.score), grad_color(self.score, agg_stats.min_score, agg_stats.max_score)) + '    ' + (' '*((STATS_FIELD_WIDTH-len(self.ending))/2)) + cstr(self.ending, COLOR1)
  200.                 + '\n' + (' '*STATS_INDENT) + '[' + cstr(self.power_slots, COLOR1) + cstr('\\', COLOR2) + cstr(self.prop_slots, COLOR1) + cstr('\\', COLOR2)
  201.                     + cstr(self.util_slots, COLOR1) + cstr('\\', COLOR2) + cstr(self.weap_slots, COLOR1) + ']'
  202.                     + (' '*(15-len(self.slot_config_plaintext())))
  203.                     + 'Rating:    ' + cstr('%3d' % self.rating, grad_color(self.rating, agg_stats.min_rating, agg_stats.max_rating))
  204.                     + '  ' + cstr('{:^19}'.format(self.location), location_color(self.location))
  205.                 + '\n' + (' '*STATS_INDENT) + 'Shots:   ' + cstr('%4d' % self.shots_fired, COLOR1)
  206.                     + '  Melee:    ' + cstr('%4d' %self.melee, COLOR1) + '  Visited:         ' + cstr('%2d' % self.visited, COLOR1)
  207.                 + '\n' + (' '*STATS_INDENT) + 'Damage         Dealt: ' + cstr('%7d' % self.damage, COLOR1) + '  Taken:      ' + cstr('%7d' % self.damage_taken, COLOR1)
  208.                 + '\n' + (' '*STATS_INDENT) + 'Max Alert:  ' + cstr(self.max_alert, COLOR1) + '  Low Sec:  ' + cstr('%3d' % self.low_sec, COLOR1)
  209.                     + cstr('%', COLOR2) + '  Hacks: ' + cstr('%3d' %  self.hacks, COLOR1)
  210.                     + ' (' + cstr(('%5.1f' % self.success_hack_perc), COLOR1) + cstr('%', COLOR2) + ')'
  211.                 + '\n' + (' '*STATS_INDENT) + 'Turns: ' + cstr('%6d' % self.turns_passed, COLOR1) + '  Time:   ' + time_html(self.play_time)
  212.                     + '  Actions:  ' + cstr('%9d' % self.actions, COLOR1)
  213.                 + '\n' + (' '*STATS_INDENT) + 'SPT: '
  214.                     + cstr('%8.2f' % self.score_per_turn, grad_color(self.score_per_turn, agg_stats.min_score_per_turn, agg_stats.max_score_per_turn))
  215.                     + '  TPM:  ' + cstr('%8.2f' % self.turns_per_min, grad_color(self.turns_per_min, agg_stats.min_turns_per_min, agg_stats.max_turns_per_min))
  216.                     + '  APM:  '
  217.                     + cstr('%13.2f' % self.actions_per_min, grad_color(self.actions_per_min, agg_stats.min_actions_per_min, agg_stats.max_actions_per_min)))
  218.  
  219.                
  220. class AggregateRunStats(object):
  221.     def __init__(self, run_list):
  222.         self.run_list = run_list
  223.        
  224.         self.max_score = max(run.score for run in self.run_list)
  225.         self.max_rating = max(run.rating for run in self.run_list)
  226.         self.max_score_per_turn = max(run.score_per_turn for run in self.run_list)
  227.         self.max_actions_per_min = max(run.actions_per_min for run in self.run_list)
  228.         self.max_turns_per_min = max(run.turns_per_min for run in self.run_list)
  229.         self.max_low_sec = max(run.low_sec for run in self.run_list)
  230.         self.max_hacks = max(run.hacks for run in self.run_list)
  231.         self.max_success_hack_perc = max(run.success_hack_perc for run in self.run_list)
  232.         self.max_damage = max(run.damage for run in self.run_list)
  233.        
  234.         self.min_score = min(run.score for run in self.run_list)
  235.         self.min_rating = min(run.rating for run in self.run_list)
  236.         self.min_score_per_turn = min(run.score_per_turn for run in self.run_list)
  237.         self.min_actions_per_min = min(run.actions_per_min for run in self.run_list)
  238.         self.min_turns_per_min = min(run.turns_per_min for run in self.run_list)
  239.         self.min_low_sec = min(run.low_sec for run in self.run_list)
  240.         self.min_hacks = min(run.hacks for run in self.run_list)
  241.         self.min_success_hack_perc = min(run.success_hack_perc for run in self.run_list)
  242.         self.min_damage = min(run.damage for run in self.run_list)
  243.    
  244.     def get_num_occurences(self, metric_list, min, max):
  245.         occurences = 0
  246.         for metric in metric_list:
  247.             if float(metric) >= min and float(metric) < max:
  248.                 occurences += 1
  249.         return occurences
  250.    
  251.     def histogram_str(self, metric_list, num_bins, html):
  252.         hist_str = ''
  253.         bin_width = 0.001 if len(metric_list) < 2 else float(max(metric_list) - min(metric_list)) / num_bins
  254.         for bin in range(num_bins, 0, -1):
  255.             bin_min = min(metric_list) + (float(bin - 1) * bin_width)
  256.             # KLUDGE - get_num_occurences is inclusive on the lower end of the range, and exclusive
  257.             # on the high end. For the highest bin only, extend the maximum of the bin interval very
  258.             # slightly to ensure the maximum metric value is captured in the number of occurrences
  259.             bin_max = (min(metric_list) + (float(bin) * bin_width) if bin != num_bins
  260.                 else min(metric_list) + (float(bin) * bin_width) + 0.001)
  261.             num_occur = self.get_num_occurences(metric_list, bin_min, bin_max)
  262.             bar_len = int(round((num_occur / float(len(metric_list)))
  263.                 * HISTOGRAM_NUM_COLUMNS))
  264.             bin_heading = ('%6.2f' % ((bin_max + bin_min) / 2))
  265.             if max(metric_list) >= 1000.0: bin_heading = ('%6.1f' % ((bin_max + bin_min) / 2))
  266.             if max(metric_list) >= 10000.0: bin_heading = ('%6.0f' % ((bin_max + bin_min) / 2))
  267.             if html:
  268.                 hist_str += ('\n' + bin_heading
  269.                     + cstr(' | ', COLOR1)
  270.                     + cstr(HISTOGRAM_BAR_CHAR*bar_len,
  271.                         grad_color((bin_max + bin_min) / 2, min(metric_list), max(metric_list))))
  272.             else:
  273.                 hist_str += ('\n' + bin_heading + ' | '
  274.                     + (HISTOGRAM_BAR_CHAR*bar_len))
  275.         if html:
  276.             hist_str += '\n' + cstr('       +' + ('-'*(HISTOGRAM_NUM_COLUMNS + 1)), COLOR1)
  277.         else:
  278.             hist_str += '\n       +' + ('-'*(HISTOGRAM_NUM_COLUMNS + 1))
  279.         hist_str += '\n         ' + '0%' + (' '*(HISTOGRAM_NUM_COLUMNS-6)) + '100%\n'
  280.         return hist_str
  281.    
  282.     def get_single_histogram(self, metric_list, histogram_title, num_bins, html):
  283.         hist_str = ''
  284.         if html:
  285.             hist_str += ('\n\n' + (' '*HISTOGRAM_TITLE_INDENT) +
  286.                 cstr(('{:^'+str(HISTOGRAM_NUM_COLUMNS)+'}').format(histogram_title), COLOR1))
  287.         else:
  288.             hist_str += ('\n' + (' '*HISTOGRAM_TITLE_INDENT) +
  289.                 ('{:^'+str(HISTOGRAM_NUM_COLUMNS)+'}').format(histogram_title))
  290.         hist_str += ('\n' + (' '*HISTOGRAM_TITLE_INDENT)
  291.             + ('{:^'+str(HISTOGRAM_NUM_COLUMNS)+'}').format('-'*(len(histogram_title)+2)))
  292.         hist_str += self.histogram_str(metric_list, num_bins, html)
  293.         return hist_str
  294.    
  295.     def get_full_histograms(self, html):
  296.         hist_str = ''
  297.         hist_str += self.get_single_histogram([run.score for run in self.run_list], 'Score Histogram',
  298.             HISTOGRAM_NUM_ROWS, html)
  299.         hist_str += self.get_single_histogram([run.rating for run in self.run_list], 'Peak Rating Histogram',
  300.             HISTOGRAM_NUM_ROWS, html)
  301.         hist_str += self.get_single_histogram([run.score_per_turn for run in self.run_list], 'Score-Per-Turn Histogram',
  302.             HISTOGRAM_NUM_ROWS, html)
  303.         hist_str += self.get_single_histogram([run.turns_per_min for run in self.run_list], 'Turns-Per-Minute Histogram',
  304.             HISTOGRAM_NUM_ROWS, html)
  305.         hist_str += self.get_single_histogram([run.actions_per_min for run in self.run_list], 'Actions-Per-Minute Histogram',
  306.             HISTOGRAM_NUM_ROWS, html)    
  307.         return hist_str
  308.        
  309.        
  310. def cstr(string, color_hex):
  311.     return '<font color="#' + color_hex + '">' + str(string) + '</font>'
  312.  
  313. def time_plaintext(play_time):
  314.     if play_time > 60:
  315.         time_str = str(play_time / 60) + 'h' + str(play_time % 60) + 'm'
  316.     else:
  317.         time_str = str(play_time) + 'm'
  318.     return ('{:>' + str(TIME_FIELD_WIDTH) + '}').format(time_str)
  319.    
  320. def time_html(play_time):
  321.     if play_time > 60:
  322.         time_str = (cstr(play_time / 60, COLOR1)  + cstr('h', COLOR2)
  323.                 + cstr(play_time % 60, COLOR1) + cstr('m', COLOR2))
  324.     else:
  325.         time_str = cstr(play_time, COLOR1)  + cstr('m', COLOR2)
  326.     return (' '*(TIME_FIELD_WIDTH - len(time_plaintext(play_time).strip()))) + time_str
  327.  
  328. def grad_color(val, min, max):
  329.     ratio = float((val - min)) / (max - min) if (max - min) != 0 else 0.0;
  330.     if ratio >= 1.0: return COL_GRAD[-1]
  331.     if ratio <= 0.0: return COL_GRAD[0]
  332.     for i, col_hex in enumerate(COL_GRAD):
  333.         if ratio <= float(i + 1) / len(COL_GRAD):
  334.             return col_hex
  335.     return FOREGROUND_COLOR
  336.    
  337. def location_color(loc):
  338.     if 'Materials' in loc or 'Scrapyard' in loc:
  339.         return '9e8664'
  340.     if 'Mines' in loc:
  341.         return '666666'
  342.     if 'Storage' in loc:
  343.         return 'b25900'
  344.     if 'Caves' in loc or 'Zion' in loc:
  345.         return '665233'
  346.     if 'Garrison' in loc:
  347.         return 'b20000'
  348.     if 'Factory' in loc:
  349.         return '9e9e9e'
  350.     if 'Research' in loc:
  351.         return 'bf00ff'
  352.     if 'Quarantine' in loc or 'Testing' in loc:
  353.         return '00b200'
  354.     if 'Armory' in loc:
  355.         return 'ff0000'
  356.     if 'Extension' in loc or 'Hub' in loc or 'Waste' in loc:
  357.         return '848400'
  358.     if 'Access' in loc:
  359.         return 'dedede'
  360.     if 'Command' in loc:
  361.         return '00a3d9'
  362.     if 'Warlord' in loc:
  363.         return 'b22d00'
  364.     return '00e100'
  365.    
  366.    
  367. if __name__ == '__main__':
  368.     # Load and sort all run stats
  369.     run_list = []
  370.     for filename in os.listdir('scores'):
  371.         if '.txt' in filename and '_log' not in filename:
  372.             with open(os.path.join('scores', filename), 'r') as f:
  373.                 try:
  374.                     run_list.append(RunStats(f))
  375.                 except:
  376.                     print 'Error parsing morgue file: ' + filename
  377.                     print traceback.format_exc()
  378.                
  379.     run_list = sorted(run_list, key=lambda run: run.score, reverse=True)
  380.     agg_stats = AggregateRunStats(run_list)
  381.     # Generate list of locations visisted
  382.     branch_set = set()
  383.     for run in run_list:
  384.         if hasattr(run, 'route'):
  385.             branch_set = branch_set | set(run.route)
  386.     branch_list = sorted(list(branch_set),
  387.         key=lambda branch: int(branch[0:branch.index('/')]) if '/' in branch else branch,
  388.         reverse=True)
  389.    
  390.     # Generate the high score plaintext output
  391.     datetime_str = datetime.datetime.now().strftime("%I:%M%p, %B %d, %Y")
  392.     out_str =  '\n -----------------------------\n'
  393.     out_str += '  --[ COGMIND HIGH SCORES ]--\n'
  394.     out_str += ' -----------------------------\n   '
  395.     out_str += datetime_str + '\n\n'
  396.     for i, run in enumerate(run_list):
  397.         if i > 0: out_str += '\n\n'
  398.         out_str += '{:<6s}'.format(str(i + 1) + '.') + str(run)
  399.     total_time = sum(run.play_time for run in run_list)
  400.     out_str += '\n\nTotal Time: ' + str(total_time / 60) + 'h' + str(total_time % 60) + 'm'
  401.     out_str += '\n\n Locations Visited'
  402.     out_str += '\n-------------------'
  403.     for branch in branch_list:
  404.         out_str += '\n' + str(branch)
  405.     out_str += '\n' + agg_stats.get_full_histograms(False)
  406.     # Write the plaintext output to file
  407.     with open(os.path.join(os.getcwd(), 'high_scores.txt'), 'w') as f:
  408.         f.write(out_str)
  409.     # Write the plaintext output to stdout
  410.     print out_str
  411.    
  412.     # Generate the high score HTML output
  413.     out_html = '<html><body text="#' + FOREGROUND_COLOR + '" bgcolor="#' + BACKGROUND_COLOR + '"><pre>'
  414.     out_html += '\n -----------------------------\n'
  415.     out_html += '  --[ ' + cstr('COGMIND HIGH SCORES', COLOR1) + ' ]--\n'
  416.     out_html += ' -----------------------------\n   '
  417.     out_html += cstr(datetime_str, COLOR1) + '\n\n'
  418.     for i, run in enumerate(run_list):
  419.         if i > 0: out_html += '\n\n'
  420.         out_html += cstr(i + 1, COLOR1) + '.' + (' '*(6-len(str(i))-1)) + run.html_str(agg_stats)
  421.     out_html += '\n\nTotal Time: ' + time_html(total_time)
  422.     out_html += '\n\n ' + cstr('Locations Visited', COLOR1)
  423.     out_html += '\n-------------------'
  424.     for branch in branch_list:
  425.         out_html += '\n' + cstr(branch, location_color(branch))
  426.     out_html += '\n' + agg_stats.get_full_histograms(True)
  427.     out_html += '</body></html>'
  428.     # Write the HTML output to file
  429.     with open(os.path.join(os.getcwd(), 'high_scores.html'), 'w') as f:
  430.         f.write(out_html)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement