from matplotlib.animation import FuncAnimation as FA
from matplotlib.patches import Polygon as mpP
from matplotlib.patches import Rectangle as mpR
import matplotlib.pyplot as plt
from math import inf as INF
from math import pi as pi
from math import sqrt as mS
import numpy as np
import pandas as pd
import seaborn as sns
#Height of unit equilateral triangle
h = mS(3)/2
def dozoArray() :
"""
Create array of 28 points.
Parameters:
None.
Returns:
List of lists, where each list corresponds to a row.
Each row contains the corresponding point(s), (x,y), 7 through 1.
"""
points = []
row = []
#Rows 0-6.
for i in range(7):
#Row vertical location.
y = (i-3) * h
#Points in row.
for x in range(i+1) :
row.append((-i/2 + x, y))
#Append row to array.
points.append(row)
#Reset row for next.
row = []
return(points)
def dozoBoard(ax, dA) :
"""
Draw Dozo Board.
Parameters:
ax : Current ax.
dA : dozoArray().
Returns:
ax : Updated ax.
"""
X = []
Y = []
for row in dA :
for p in row:
X.append(p[0])
Y.append(p[1])
ax.scatter(X, Y, c = '#BBBBBBFF', s = 100)
return(ax)
def clearAx(ax):
"""
Clear axes.
Parameters:
ax - Current ax.
Returns:
ax - Updated ax.
"""
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["left"].set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
return(ax)
def repeatTriangle(s) :
"""
Repeat all triangles of the same size and orientation as a given triangle across
a Dozo Board.
Initial triangle is as far left and top as possible for that size and orientation.
Parameters:
s = [t, r]
t - Initial triangle indices, as far left and top as possible.
r - Rows in which the triangle will repeat. This results in (r^2+r) / 2 triangles.
Returns:
List of (r^2+r) / 2 lists, each containing 3 tuples (x,y).
"""
t0 = s[0]
r = s[1]
l = s[2]
repeats = []
#Rows.
for y in range(r) :
#Columns.
for x in range(r-y) :
triangle = []
#Indices.
for i in t0 :
#Vertex.
v = dA[i[1]-y][i[0]+x]
#3 points.
triangle.append((v[0], v[1]))
#Append the triangle.
repeats.append([triangle, l])
return(repeats)
def getSideLength(l, o) :
"""
Determine the side length. For output.
Parameters:
l - index
o - option.
Returns:
String of side length.
"""
if o == 1 :
return(str(l))
elif o == 2 :
n = (2*l+3)**2 + 4
return(str('√' + str(n) + '/2'))
elif o == 3 :
c = ''
if l == 2:
c = '2'
return(c + str('√3'))
def allTriangles() :
"""
All the triangles. 120 triangles from 3 groups, each having two subgroups.
Parameters:
None.
Returns:
List of lists, where each list corresponds to 3 points (x,y) of an equilateral triangle.
"""
#Metadata of triangle to be passed to repeatTriangle()
setup = []
#Final list of triangles.
triangles = []
#Points up/down, triangle length l = [1,2,3,4,5,6]
#21+15(2)+10+6(2)+3+1(2) = 78 Triangles.
for l in range(1, 7) :
#Down.
setup.append([[(0, 6), (l, 6), (0, 6-l)], 7-l, getSideLength(l, 1)])
if l < 4 :
#Up.
setup.append([[(l, 6), (l, 6-l), (0, 6-l)], 7-2*l, getSideLength(l, 1)])
#Points uppish, triangle length l = [√29/2, √53/2, √85/2]
#6(2)+3(2)+1(2) = 20 Triangles.
for l in range(1, 4) :
#Up and right.
setup.append([[(l+2, 6), (1, 4-l), (0, 5)], 4-l, getSideLength(l, 2)])
#Up and left.
setup.append([[(1, 6), (l+2, 5), (0, 4-l)], 4-l, getSideLength(l, 2)])
#Points left/right, triangle length l = [√3, 2√3].
#10(2)+1(2) = 22 Triangles.
for l in [1, 2] :
setup.append([[(l, 6), (2*l, 6-l), (0, 6-2*l)], 7-3*l, getSideLength(l, 3)])
setup.append([[(2*l, 6), (l, 6-2*l), (0, 6-l)], 7-3*l, getSideLength(l, 3)])
for s in setup :
for t in repeatTriangle(s) :
triangles.append(t)
return(triangles)
def drawTriangle(xy) :
"""
Draw an equilateral triangle.
Parameters:
xy - List of 3 tuples that correspond to points (x,y).
Returns:
Polygon patch.
"""
return(mpP(xy,
edgecolor = 'k',
facecolor = 'None',
lw = 2,
zorder = 2))
def plotHelper(i, triangles) :
"""
Plot an equilateral triangle on the Dozo Board.
Parameters:
i - Index of triangle from list.
Returns:
Plot.
"""
ax = fig.add_subplot(xlim = (-3.5, 4.5),
ylim = (-3, 3))
fig.subplots_adjust(left = 0, bottom = 0, right = 1, top = 1)
#Remove axes and ticks.
ax = clearAx(ax)
#Dozo Board.
ax = dozoBoard(ax, dA)
data = triangles[i]
t = data[0]
length = data[1]
#Equilateral triangle.
#Highlight points.
ax.scatter([t[0][0], t[1][0], t[2][0]],
[t[0][1], t[1][1], t[2][1]],
c = 'k', s = 200)
#Draw sides.
ax.add_patch(drawTriangle(t))
#Annotation.
text = [("Triangles :", (4.1, 0.8)),
(str(i+1), (4.1, 0.3)),
("Side Length : ", (4.1, -1.1)),
(length, (4.1, -1.6))]
for t in text :
plt.annotate(text = t[0],
xy = t[1],
c = 'k',
size = 24,
ha = 'right')
dA = dozoArray()
triangles = allTriangles()
#Animate
sns.set()
fig = plt.figure(figsize = (10, 7.5))
def animate(i):
plt.clf()
plotHelper(i, triangles)
#Run animation.
anim = FA(fig, animate, frames = 120, interval = 350)
#Save animation.
anim.save('2025.07.04Fiddler.mp4');
def drawStar(xy, ec, fc) :
"""
Draw a pentagram.
Parameters:
xy : (x, y), center of pentagram.
c : color of pentagram.
Returns:
Pentagram patch.
"""
x = xy[0]
y = xy[1]
#Create the angles of a pentagram
a = np.linspace(pi/2, 9*pi/2, 6)
#Compute the vertices circle
points = [(x + 0.25*np.cos(t), y + 0.25*np.sin(t)) for t in a]
return(mpP(xy = points,
edgecolor = ec,
facecolor = fc,
lw = 0,
zorder = 2))
def distance(a, b) :
"""
Distance between two points.
Parameters:
a : (x0, y0).
b : (x1, y1).
Returns:
Distance between the two points.
"""
return(mS((a[0]-b[0])**2 + (a[1]-b[1])**2))
def slope(a, b) :
"""
Slope between two points.
Parameters:
a : (x0, y0).
b : (x1, y1).
Returns:
Slope between the two points.
"""
#∞.
if a[1]-b[1] == 0 :
return(INF)
#Real.
else :
return((a[0]-b[0])/(a[1]-b[1]))
def flagArray() :
"""
Create flag array of 50 points.
Parameters:
None.
Returns:
List of 50 tuples (x,y).
"""
Xs = [(x-0.5) for x in range(-2, 4)] + [x for x in range(-2, 3)]
Ys = [y for y in range(2, -3, -1)]
points = []
for y in Ys :
#Rows of 6 stars.
for x in Xs[0:6] :
points.append((x, y))
#Offcenter rows of 5 stars.
if y > -2 :
for x in Xs[6:11] :
points.append((x, y-0.5))
return(points)
def flagHelper(ax, XY) :
"""
Draw array of 50 stars on blue background.
Parameters:
ax : Current ax.
XY : flagArray().
Returns:
ax : Current ax.
"""
ax.add_patch(mpR(xy = (-3.5, -2.8),
width = 8.4,
height = 5.6,
edgecolor = "#00276AFF",
facecolor = "#00276AFF",
zorder = 1))
for xy in XY :
ax.add_patch(drawStar(xy, 'None', 'w'))
return(ax)
def allParallelograms(XY) :
"""
All the parallelograms.
Parameters:
XY : List of 50 tuples.
Returns:
List of lists, where each list corresponds to 4 points (x,y) of a parallelogram.
"""
Ps = []
#Top point must be from top left star to 2nd last star on 2nd last row.
for i0 in range(0, 43) :
p0 = XY[i0]
#2nd point must be from after 1st point to last star on 2nd last row.
for i1 in range(i0+1, 44) :
p1 = XY[i1]
#3rd point must be from after 2nd point to 2nd last star on last row.
for i2 in range(i1, 49) :
p2 = XY[i2]
#4th point must be from after 3nd point to last star on last row.
for i3 in range(i2, 50) :
p3 = XY[i3]
#Determine characteristics.
d0 = distance(p0, p1)
m0 = slope(p0, p1)
d1 = distance(p0, p2)
m1 = slope(p0, p2)
d2 = distance(p0, p3)
m2 = slope(p0, p3)
d3 = distance(p1, p2)
m3 = slope(p1, p2)
d4 = distance(p1, p3)
m4 = slope(p1, p3)
d5 = distance(p2, p3)
m5 = slope(p2, p3)
#Opposite sides are congruent.
#Opposite sides have the same slope.
#The four points do not lie on a diagonal, opposite sides have different slopes.
if d0 == d5 and d1 == d4 and m0 == m5 and m1 == m4 and m0 != m1:
#It's a parallelogram!
Ps.append([p0, p1, p3, p2])
return(Ps)
def plotHelper2(i):
"""
Draw a single parallelogram on 4 stars of the flag array.
Parameters:
i : Parallelogram index.
Returns:
Plot.
"""
ax = fig.add_subplot(xlim = (-3.5, 4.9),
ylim = (-2.8, 2.8))
fig.subplots_adjust(left = 0, bottom = 0, right = 1, top = 1)
#Remove axes and ticks.
ax = clearAx(ax)
#Mark stars.
ax = flagHelper(ax, XY)
#Get parallelogram vertices.
para = Ps[i]
for xy in para :
#Paint the four stars red.
ax.add_patch(drawStar(xy, '#C00A31FF', '#C00A31FF'))
#Draw the paralellogram.
ax.add_patch(mpP(xy = para , edgecolor = '#C00A31FF', facecolor = 'None', lw = 2, zorder = 2))
#Annotation.
text = [("Parallelograms :", (4.88, 0.35)),
(str(i+1), (4.58, -0.15))]
for t in text :
plt.annotate(text = t[0],
xy = t[1],
c = 'w',
size = 24,
ha = 'right',
zorder = 2)
XY = flagArray()
Ps = allParallelograms(XY)
#Animate
sns.set()
fig = plt.figure(figsize = (10, 6.67))
def animate(i):
plt.clf()
plotHelper2(i)
#Run animation.
anim = FA(fig, animate, frames = 5918, interval = 25)
#Save animation.
anim.save('2025.07.04EC.mp4');