#!/usr/bin/env pypy3 import sys, re from datetime import datetime def parse_realtime(text): try: date = datetime.strptime(text, "%Y/%m/%d %H:%M:%S") except ValueError: date = datetime.strptime(text, "%Y-%m-%d %H:%M:%S") return date.timestamp() def parse_gametime(text): parts = text.split(":"); return int(parts[0]) * 60 + int(parts[1]) class StateTracker: def __init__(self): self.hist = list() self.hist_pings = list() self.slots = [False] * 64; self.time_ref = None self.time = None self.time_last = None self.pcount = None self.pcount_last = None self.pings = list() def update(self): if self.time_last and self.time > self.time_last: self.hist.append((self.time_last, self.pcount_last)) self.time_last = self.time self.pcount_last = self.pcount def finish(self): if self.time != self.time_last or \ self.pcount != self.pcount_last: self.hist.append((self.time, self.pcount)) if len(self.pings): self.hist_pings.append((self.time_ref, self.pings)) self.pings = list() def ev_begin(self, realtime): self.slots = [False] * 64 self.time_ref = parse_realtime(realtime) self.time = self.time_ref self.pcount = 0 self.update() if len(self.pings): self.hist_pings.append((self.time_ref, self.pings)) self.pings = list() def ev_connect(self, gametime, slot): if self.slots[slot]: return self.slots[slot] = True self.time = self.time_ref + parse_gametime(gametime) self.pcount += 1 self.update() if self.pcount > 64: raise ValueError("too many players") def ev_disconnect(self, gametime, slot): self.slots[slot] = False self.time = self.time_ref + parse_gametime(gametime) self.pcount -= 1 self.update() def ev_endgame_stat(self, gametime, score, ping): if ping == "999": return game_length = parse_gametime(gametime) self.pings.append((int(ping), game_length)) class WeightedMean: def __init__(self): self.samples = list() self.total = 0 self.total_weighted = 0 self.weights = 0 def feed(self, sample, weight): self.samples.append((sample, weight)) self.total += sample self.total_weighted += sample * weight; self.weights += weight def mean(self): if self.weights != 0: return self.total/ self.weights else: return 0 def wmean(self): if self.weights != 0: return self.total_weighted / self.weights else: return 0 # weighted standard deviation # http://www.itl.nist.gov/div898/software/dataplot/refman2/ch2/weightsd.pdf def wsd(self): if len(self.samples) <= 1: return 99999 wmean = self.wmean() S = 0 for sample in self.samples: S += sample[1] * (sample[0] - wmean) ** 2 N = len(self.samples) wsd = (S / ((N - 1) / N * self.weights)) ** 0.5; return wsd # standard deviation of the weighted mean def wmsd(self): if self.weights == 0: return 99999 # sum of squared weights sq_weights = 0 for sample in self.samples: sq_weights += sample[1] ** 2 # unweighted variance mean = self.mean() var = 0 for sample in self.samples: var += (sample[0] - mean) ** 2 var /= len(self.samples) wmsd = (sq_weights / (self.weights) ** 2 * var) ** 0.5 return wmsd class Day: def __init__(self, date): self.date = date self.pcount_sum = 0 self.pcount_time = 0 self.pcount_peak = 0 self.pings = WeightedMean() def avg_pcount(self): return self.pcount_sum / self.pcount_time def peak_pcount(self): return self.pcount_peak def ping_distrib(self): above_60 = 0 above_110 = 0 above_160 = 0 above_210 = 0 above_260 = 0 for sample in self.pings.samples: if sample[0] > 60: above_60 += sample[1] if sample[0] > 110: above_110 += sample[1] if sample[0] > 160: above_160 += sample[1] if sample[0] > 210: above_210 += sample[1] if sample[0] > 260: above_260 += sample[1] if len(self.pings.samples): above_60 /= self.pings.weights above_110 /= self.pings.weights above_160 /= self.pings.weights above_210 /= self.pings.weights above_260 /= self.pings.weights return "%f %f %f %f %f" % (above_60, above_110, above_160, \ above_210, above_260) class Analyzer: def __init__(self): self.time_last = None self.pcount_last = None self.days = dict() def feed(self, time, pcount): if self.time_last == None: self.time_last = time self.pcount_last = pcount return date = datetime.fromtimestamp(time).date() if date not in self.days: self.days[date] = Day(date) delta = time - self.time_last self.days[date].pcount_sum += delta * self.pcount_last self.days[date].pcount_time += delta if pcount > self.days[date].pcount_peak: self.days[date].pcount_peak = pcount self.time_last = time self.pcount_last = pcount def feed_pings(self, time, pings): date = datetime.fromtimestamp(time).date() if date not in self.days: self.days[date] = Day(date) for ping in pings: self.days[date].pings.feed(ping[0], ping[1]) def finish(self): for date, day in self.days.items(): if day.pcount_time < 80000: continue print("%s %f %f %f %f %s %d" % (date, day.avg_pcount(), \ day.pings.wmean(), day.pings.wmsd(), day.pings.wsd(), \ day.ping_distrib(), day.peak_pcount())) pass def decoder(raw): return raw.decode("ISO-8859-1") def main(): state = StateTracker() re_realtime = re.compile("^\s*\d+:\d\d RealTime: (.*)$") re_connect = re.compile("^\s*(\d+:\d\d) ClientConnect: ([0-9]+)") re_disconnect = re.compile("^\s*(\d+:\d\d) ClientDisconnect: ([0-9]+)") re_endgame_stat = re.compile("^\s*(\d+:\d\d) score: (-?[0-9]+) ping: ([0-9]+)") for (i, line) in enumerate(map(decoder, sys.stdin.buffer)): try: if "RealTime" in line: rv = re.search(re_realtime, line) if rv: state.ev_begin(rv.group(1)) continue elif "ClientConnect" in line: rv = re.search(re_connect, line) if rv: state.ev_connect(rv.group(1), int(rv.group(2))) continue elif "ClientDisconnect" in line: rv = re.search(re_disconnect, line) if rv: state.ev_disconnect(rv.group(1), int(rv.group(2))) continue elif "score:" in line: rv = re.search(re_endgame_stat, line) if rv: state.ev_endgame_stat(rv.group(1), \ rv.group(2), \ rv.group(3)) continue except: print("ERROR on line %d:" % (i + 1), file=sys.stderr) raise state.finish() analyzer = Analyzer() for (time, count) in state.hist: analyzer.feed(time, count) for (time, pings) in state.hist_pings: analyzer.feed_pings(time, pings) analyzer.finish() main()