import json import math from pathlib import Path from cairo import Context, LineCap, LineJoin, PSSurface, SVGSurface, Gradient, LinearGradient # from scipy.integrate import quad from island.match import Match class Point: def __init__(self, x, y): self.x = x self.y = y def move_to(self, newx, newy): self.x = newx self.y = newy def rel_move_to(self, offx, offy): self.x += offx self.y += offy def new_rel_point(self, offx, offy): return Point(self.x + offx, self.y + offy) class Color: def __init__(self, r = 0, g = 0, b = 0, a = 0): self.r = r self.g = g self.b = b self.a = a def shade(self, percentage): return Color(self.r, self.g, self.b, 0.8 * percentage + 0.2) def set_color(ctx, color): """ :param ctx: context :param color: the Color """ ctx.set_source_rgba(color.r, color.g, color.b, color.a) def draw_alive_face(ctx, pt, r, w): """ :param ctx: context :param pt: the center point :param r: radius :param w: line width """ ctx.new_path() ctx.set_source_rgba(1, 1, 1, 1) ctx.set_line_width(w) ctx.arc(pt.x, pt.y, r, 0, 2*math.pi) ctx.fill() ctx.set_source_rgba(0, 0, 0, 1) ctx.arc(pt.x, pt.y, r, 0, 2*math.pi) ctx.stroke() ctx.arc(pt.x - r / SQRT2 / 2, pt.y - r / SQRT2 / 2, r / 10, 0, 2 * math.pi) ctx.fill() ctx.arc(pt.x + r / SQRT2 / 2, pt.y - r / SQRT2 / 2, r / 10, 0, 2 * math.pi) ctx.fill() ctx.set_line_cap(LineCap.ROUND) ctx.arc(pt.x, pt.y, r * GOLDEN_SECTION, math.pi / 4, math.pi / 4 * 3) ctx.stroke() def draw_dead_face(ctx, pt, r, w): """ :param ctx: context :param pt: the center point :param r: radius :param w: line width """ ctx.set_line_cap(LineCap.ROUND) ctx.set_line_join(LineJoin.ROUND) ctx.new_path() ctx.set_source_rgba(1, 1, 1, 1) ctx.set_line_width(w) ctx.arc(pt.x, pt.y, r, 0, 2*math.pi) ctx.fill() ctx.set_source_rgba(0.5, 0.5, 0.5, 1) ctx.arc(pt.x, pt.y, r, 0, 2*math.pi) ctx.stroke() offset = r / 6 * SQRT3 / 2 ctx.move_to(pt.x - r / SQRT2 / 2 + offset, pt.y - r / SQRT2 / 2 + r / 6) ctx.rel_line_to(- r / 6 * SQRT3, - r / 6) ctx.move_to(pt.x - r / SQRT2 / 2 + offset, pt.y - r / SQRT2 / 2 + r / 6) ctx.rel_line_to(- r / 6 * SQRT3, r / 6) ctx.move_to(pt.x + r / SQRT2 / 2 - offset, pt.y - r / SQRT2 / 2 + r / 6) ctx.rel_line_to(r / 6 * SQRT3, - r / 6) ctx.move_to(pt.x + r / SQRT2 / 2 - offset, pt.y - r / SQRT2 / 2 + r / 6) ctx.rel_line_to(r / 6 * SQRT3, r / 6) ctx.stroke() a = r * GOLDEN_SECTION ctx.move_to(pt.x - a / SQRT2, pt.y + a / SQRT2) step = a / (4 * SQRT2) for _ in range(4): ctx.rel_line_to(step, -step) ctx.rel_line_to(step, step) ctx.stroke() def bezier_diff(x, cax, cbx, ccx, cay, cby, ccy): return math.sqrt((cax + cbx * x + ccx * (x ** 2)) ** 2 + (cay + cby * x + ccy * (x ** 2)) ** 2) def calc_bezier_diff_angle(p0, p1, p2, p3, al): """ f(x) = a - 3ax + 3ax^2 - ax^3 + 3bx - 6bx^2 + 3bx^3 + 3cx^2 - 3cx^3 + dx^3 f'(x) = (-3a+3b) + 2(3a - 6b +3c)x + 3(-a+3b-3c+d)x^2 """ # cax = -3 * p0.x + 3 * p1.x # cbx = 6 * p0.x - 12 * p1.x + 6 * p2.x # ccx = -3 * p0.x + 9 * p1.x - 9 * p2.x + 3 * p3.x # cay = -3 * p0.y + 3 * p1.y # cby = 6 * p0.y - 12 * p1.y + 6 * p2.y # ccy = -3 * p0.y + 9 * p1.y - 9 * p2.y + 3 * p3.y # s = quad(bezier_diff, 0, 1, args=(cax, cbx, ccx, cay, cby, ccy)) # t = 1 - al / s[0] * 1.1 # bx = p0.x * (1-t) ** 3 + 3 * p1.x * t * (1-t) ** 2 + 3 * p2.x * t ** 2 * (1-t) + p3.x * t ** 3 # by = p0.y * (1-t) ** 3 + 3 * p1.y * t * (1-t) ** 2 + 3 * p2.y * t ** 2 * (1-t) + p3.y * t ** 3 # a = math.asin((by - p3.y) / math.sqrt((bx - p3.x) ** 2 + (by - p3.y) ** 2)) # if bx < p3.x: # a = math.pi - a a = math.pi * 5 / 4 if p0.x < p3.x else math.pi / 4 return a - math.pi / 30 def draw_action_arrow(ctx, cf, ct, w, al, r, c): """ :param ctx: context :param cf: from the center point :param ct: to the center point :param w: line width :param al: arrow length :param r: radius :param c: Color """ ctx.set_source_rgba(c.r, c.g, c.b, c.a) ctx.set_line_width(w) ctx.set_line_cap(LineCap.ROUND) ctx.set_line_join(LineJoin.ROUND) ltr = 1 if cf.x < ct.x else -1 af = Point(cf.x + ltr * r / SQRT2, cf.y - ltr * r / SQRT2) at = Point(ct.x - ltr * r / SQRT2, ct.y - ltr * r / SQRT2) ctx.move_to(af.x, af.y) extend = (ltr * (ct.x - cf.x)) ** 0.7 p1 = Point(af.x + ltr * extend / SQRT2, af.y - ltr * extend / SQRT2) p2 = Point(at.x - ltr * extend / SQRT2, at.y - ltr * extend / SQRT2) ctx.curve_to(p1.x, p1.y, p2.x, p2.y, at.x, at.y) a = calc_bezier_diff_angle(af, p1, p2, at, al) ctx.move_to(at.x, at.y) ctx.line_to(at.x + al * math.cos(a - math.pi / 12), at.y + al * math.sin(a - math.pi / 12)) ctx.move_to(at.x, at.y) ctx.line_to(at.x + al * math.cos(a + math.pi / 12), at.y + al * math.sin(a + math.pi / 12)) ctx.stroke() def draw_legend_arrow(ctx, cf, ct, w, al, c): """ :param ctx: context :param cf: from the center point :param ct: to the center point :param w: line width :param al: arrow length :param c: Color """ ctx.set_source_rgba(c.r, c.g, c.b, c.a) ctx.set_line_width(w) ctx.set_line_cap(LineCap.ROUND) ctx.set_line_join(LineJoin.ROUND) ctx.move_to(cf.x, cf.y) ctx.line_to(ct.x, ct.y) ctx.line_to(ct.x - al * math.cos(-math.pi / 12), ct.y + al * math.sin(-math.pi / 12)) ctx.move_to(ct.x, ct.y) ctx.line_to(ct.x - al * math.cos(math.pi / 12), ct.y + al * math.sin(math.pi / 12)) ctx.stroke() def draw_food_bar(ctx, refp, food, foodi, w, h): """ :param ctx: context :param refp: reference point, left bottom :param food: food :param foodi: initial food :param w: bar width :param h: bar height """ ctx.set_line_width(w / 10) ctx.set_line_cap(LineCap.SQUARE) ctx.set_line_join(LineJoin.MITER) if food > 0: foodh = food / foodi * h ctx.rectangle(refp.x, refp.y - foodh, w, foodh) set_color(ctx, TAMAMOROKOSHI) ctx.fill() ctx.rectangle(refp.x, refp.y - h, w, h) ctx.set_source_rgba(0, 0, 0, 1) ctx.stroke() def draw_linear_gradient_rect(ctx, tl, br, lw, start, stop, horizonal=True): """ :param ctx: context :param tl: the top-left reference point :param br: the right-bottom reference point :param lw: line width :param start: start color :param stop: stop color :param horizonal: if the gradient is horizonal """ ctx.set_line_cap(LineCap.SQUARE) ctx.set_line_join(LineJoin.MITER) # source = ctx.get_source() lr = LinearGradient(tl.x, tl.y, br.x, br.y) lr.add_color_stop_rgba(0, start.r, start.g, start.b, start.a) lr.add_color_stop_rgba(1, stop.r, stop.g, stop.b, stop.a) ctx.set_source(lr) ctx.rectangle(tl.x, tl.y, br.x - tl.x, br.y - tl.y) ctx.fill() # ctx.rectangle(tl.x, tl.y, br.x - tl.x, br.y - tl.y) # ctx.set_line_width(lw) # ctx.set_source_rgba(0, 0, 0, 1) # ctx.stroke() def draw_faces(ctx, p, f, r, lw): """ :param ctx: context :param p: positions :param f: foods :param r: radius :param lw: line width """ for k in p.keys(): if f[k] > 0: draw_alive_face(ctx, p[k], r, lw) else: draw_dead_face(ctx, p[k], r, lw) draw_food_bar(ctx, p[k].new_rel_point(r / SQRT2 + 20, r / SQRT2), f[k], 5, 10, r * SQRT2) # draw_text(ctx, str(k), Point(p[k].x, p[k].y - r - 15), Point(p[k].x, p[k].y - r - 5), CENTER_ALIGNED|MIDDLE_ALIGNED, 12) def draw_text(ctx, text, tl, br, align, font_size=None, color=Color(0,0,0,1), font_face=None, font_matrix=None, font_options=None): """ draw text :param ctx: context :param text: the text :param tl: the top-left reference point :param br: the right-bottom reference point :param align: alignment :param font_size: float font size :param color: text color :param font_face: font face :param font_matrix: font matrix :param font_options: font options """ if font_size is not None: ctx.set_font_size(font_size) if font_face is not None: ctx.set_font_face(font_face) if font_matrix is not None: ctx.set_font_matrix(font_matrix) if font_options is not None: ctx.set_font_options(font_options) te = ctx.text_extents(text) ha = align & 0b00001111 if ha == CENTER_ALIGNED: x = (br.x + tl.x - te.width) / 2 - te.x_bearing / 2 elif ha == RIGHT_ALIGNED: x = br.x - te.width else:# ha == LEFT_ALIGNED x = tl.x - te.x_bearing va = align & 0b11110000 if va == MIDDLE_ALIGNED: y = (br.y + tl.y + te.height) / 2 - (te.height + te.y_bearing) / 2 elif va == BOTTOM_ALIGNED: y = br.y else: #va == TOP_ALIGNED: y = tl.y + te.height - (te.height + te.y_bearing) ctx.move_to(x, y) set_color(ctx, color) ctx.show_text(text) LEFT_ALIGNED = 0b00000001 CENTER_ALIGNED = 0b00000010 RIGHT_ALIGNED = 0b00000100 TOP_ALIGNED = 0b00010000 MIDDLE_ALIGNED = 0b00100000 BOTTOM_ALIGNED = 0b01000000 GOLDEN_SECTION = 0.6180339887 SQRT2 = 1.4142135623 SQRT3 = 1.7320508076 FACE_WIDTH = 100 FACE_INTERVAL = 50 ROUND_HEIGHT = 300 LINEWIDTH = 4 ROUND_NO_WIDTH = 75 TAMAMOROKOSHI = Color(0.9098039215686275, 0.7137254901960784, 0.2784313725490196, 1) AKE = Color(0.8, 0.3294117647058824, 0.2274509803921569, 1) TOKIWA = Color(0.1058823529411765, 0.5058823529411765, 0.2431372549019608, 1) def draw_picture(fin,fout): M = Match.read_from_json(fin) info = M.query('game', 'created').select('info').first()['info'] conf = json.loads(info['config']) game_end_at = int(info['game_end_at']) P = {} # position F = {} # face R = 30 for r in M.query('player', 'join').raw_data: P[r['pid']] = Point(len(F) * FACE_WIDTH + FACE_INTERVAL + ROUND_NO_WIDTH, ROUND_HEIGHT / 2) F[r['pid']] = 5 surface = SVGSurface(fout, (len(P) + 1) * (FACE_WIDTH) + ROUND_NO_WIDTH, (game_end_at + 2) * ROUND_HEIGHT) # surface = PSSurface(fout, (len(P)+1)*(FACE_WIDTH) + ROUND_NO_WIDTH, (game_end_at+2)*ROUND_HEIGHT) # surface.set_eps(True) context = Context(surface) for i in range(1, game_end_at + 2): next_f = F.copy() draw_text(context, "R# %d" % i, Point(5, ROUND_HEIGHT/2+(i-1)*ROUND_HEIGHT), Point(ROUND_NO_WIDTH, ROUND_HEIGHT/2+(i-1)*ROUND_HEIGHT), LEFT_ALIGNED | MIDDLE_ALIGNED, 40) for r in M.query('action', 'done').where(lambda row: row['rno'] == i).raw_data: draw_action_arrow(context, P[r['a']], P[r['b']], LINEWIDTH, 15, R, AKE.shade(r['tr'] / 1440) if r['act_a'] == 'D' else TOKIWA.shade(r['tr']/1440)) draw_action_arrow(context, P[r['b']], P[r['a']], LINEWIDTH, 15, R, AKE.shade(r['tr'] / 1440) if r['act_b'] == 'D' else TOKIWA.shade(r['tr']/1440)) next_f[r['a']] += conf['payoffs']["%s%s"%(r['act_a'], r['act_b'])][0] * r['tr'] / 1440.0 next_f[r['b']] += conf['payoffs']["%s%s"%(r['act_a'], r['act_b'])][1] * r['tr'] / 1440.0 draw_faces(context, P, F, R, LINEWIDTH) for k in next_f.keys(): F[k] = next_f[k] - conf['rounds']['consumption'] P[k].rel_move_to(0, ROUND_HEIGHT) # draw legend draw_legend_arrow(context, Point(50, ROUND_HEIGHT * (game_end_at + 1)), Point(75, ROUND_HEIGHT * (game_end_at + 1)), LINEWIDTH, 15, TOKIWA) draw_legend_arrow(context, Point(50, ROUND_HEIGHT * (game_end_at + 1) + 30), Point(75, ROUND_HEIGHT * (game_end_at + 1) + 30), LINEWIDTH, 15, AKE) draw_text(context, 'C', Point(100, ROUND_HEIGHT * (game_end_at + 1)), Point(120, ROUND_HEIGHT * (game_end_at + 1)), LEFT_ALIGNED|TOP_ALIGNED, 30) draw_text(context, 'D', Point(100, ROUND_HEIGHT * (game_end_at + 1) + 30), Point(120, ROUND_HEIGHT * (game_end_at + 1) + 30), LEFT_ALIGNED|TOP_ALIGNED, 30) draw_linear_gradient_rect(context, Point(200, ROUND_HEIGHT * (game_end_at + 1)), Point(350, ROUND_HEIGHT * (game_end_at + 1) + 20), 2, TOKIWA, TOKIWA.shade(0)) draw_linear_gradient_rect(context, Point(200, ROUND_HEIGHT * (game_end_at + 1)+30), Point(350, ROUND_HEIGHT * (game_end_at + 1) + 50), 2, AKE, AKE.shade(0)) surface.finish() if __name__ == '__main__': for file in Path('wos-data-compete').iterdir(): p = Path(file) if p.suffix == '.json': name = p.stem draw_picture(str(p), str(p.parent.parent / 'graph' / ("%s.svg"%name))) # draw_picture("wos-data-new/G285.json", "graph/G285.eps")