358 lines
13 KiB
Python
358 lines
13 KiB
Python
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")
|