In the squash doubles rule book virtually everything is looked at in two dimensions (2-D). But the court is pretty obviously a three dimensional (3-D) space. My friend, in one of his posts, was suggesting that what would look like interference in a 2-D view would not be so if looked at properly in a 3-D view. I decided to see if I could produce images to investigate that suggestion. Hence the two previous posts on producing an isometric projection of a hardball doubles squash court.

So let’s see where we can get with this attempted visual improvement. I am only going to look at front wall interference. Don’t want to complicate the math or code any more than necessary.

Ball and The Pyramid

Let’s start by adding a ball to the isometric projection. The center of the ball will be 8 feet 6 inches above the floor and right over the t-line (aka short line). I am also adding code for dotted lines down to the floor and then to the nearest side wall if the ball is not on the t-line.

... ...
  p_ball = [8, 15, 7.5]
... ...
  b_x, b_y = toIsometric2D(*p_ball, _iso_angle)
  ax.scatter(b_x, b_y, c='b')
  bf_x, bf_y = toIsometric2D(p_ball[0], p_ball[1], 0, _iso_angle)
  ax.plot([bf_x, b_x], [bf_y, b_y], c='b', ls='--')
  if p_ball[1] != 15:
    if p_ball[0] <= 12.5:
      bl_x, bl_y = toIsometric2D(0, p_ball[1], 0, _iso_angle)
      ax.plot([bl_x, bf_x], [bl_y, bf_y], c='b', ls='--')
    else:
      br_x, br_y = toIsometric2D(25, p_ball[1], 0, _iso_angle)
      ax.plot([br_x, bf_x], [br_y, bf_y], c='b', ls='--')
isometric view of a hardball doubles court with a ball 8.5 feet above the t-line
Isometric view of a hardball doubles court with a ball 8.5 feet
above the t-line.

The rules are constantly talking about the triangle. A 2-D figure. For this situation we are going to look at the 3-D equivalent, a polyhedron. More commonly, a pyramid.

To that end I will draw lines from the center of the ball to the four corners of the playable portion of the front wall. That is a bit of an exaggeration on my part. The ball is not a point, so I should likely be reducing the area on the front wall to account for the balls diameter. But, I am a little lazy and my compromise will not really significantly affect the discussion or visualizations.

I am using a matplotlib Polygon to produce a coloured triangle on the bottom of the pyramid. Since that is really the triangle that will determine whether or not an opponent is interfering with the strikers rights. So in fact, a pyramid isn’t really required. We just need a 3-D trianglar plane. But…

ax.plot([b_x, iso_1[12][0]], [b_y, iso_1[12][1]], c='b')
ax.plot([b_x, iso_1[13][0]], [b_y, iso_1[13][1]], c='b')
ax.plot([b_x, iso_1[16][0]], [b_y, iso_1[16][1]], c='b')
ax.plot([b_x, iso_1[17][0]], [b_y, iso_1[17][1]], c='b')
t1 = plt.Polygon([[b_x, b_y], [iso_1[16][0], iso_1[16][1]], [iso_1[17][0], iso_1[17][1]]], color='b', alpha=.3)
plt.gca().add_patch(t1)
isometric view of a hardball doubles court with a ball and resulting 3-D pyramid
Isometric view of a hardball doubles court with a ball and
resulting 3-D pyramid.

Not quite the view I was hoping for. But, let’s add an opponent and see what we can do.

Opponent

I am going to add the opponent 6 feet from the left wall and 24 feet from the back wall. They will be 6 feet 1 inch tall. Along with dashed lines to help show their location. I am going to use ellipses, top and bottom, joined by lines to symbolize the opponent. Sorry no head. I am assuming the opponent is 2 feet wide and 1 foot thick. And I will apply the foreshortening value to those when plotting the opponent on the projection.

# opponent
o_x_1, o_y_1 = toIsometric2D(*p_oppo, _iso_angle)
o_x_2, o_y_2 = toIsometric2D(p_oppo[0], p_oppo[1], p_ht, _iso_angle)
o_lf_x, o_lf_y = toIsometric2D(0, p_oppo[1], 0, _iso_angle)
o_t_x, o_t_y = toIsometric2D(p_oppo[0], 15, 0, _iso_angle)
ax.plot([o_lf_x, o_x_1], [o_lf_y, o_y_1], c='g', ls="--")
ax.plot([o_t_x, o_x_1], [o_t_y, o_y_1], c='g', ls="--")

pe1 = patches.Arc((o_x_1, o_y_1), 2*.816, .816, color='g')
ax.add_patch(pe1)
pe2 = patches.Arc((o_x_2, o_y_2), 2*.816, .816, color='g')
ax.add_patch(pe2)
ax.plot()
ax.plot([o_x_1 - .816, o_x_2 - .816], [o_y_1, o_y_2], c='g')
ax.plot([o_x_1 + .816, o_x_2 + .816], [o_y_1, o_y_2], c='g')

And, I get the following image.

isometric view of a hardball doubles court with a ball, resulting 3-D pyramid and symbolize opponent
Isometric view of a hardball doubles court with a ball,
resulting 3-D pyramid, and symbolized opponent.

Certainly not what I was hoping for. Let’s see if there’s a way to help.

Geometry

I decided I would sort the formulae for the two sides of the lower triangle and then for its height based on the position along the side wall. Figured that would tell me if the opponent was interfering with the striker’s shot. For the projection, I would colour the part of the opponent below the triangular plane in green and above the plane in red.

Just so you know, I started out making a few big mistakes. First of all I started out generating the formulae using the relevant points in the isometric projection. Took me some time to realize that was a serious mistake. Then to determine whether or not the opponent was crossing the plane, I ended up using the wrong y value to determine the height of the plane. I was using the opponents x location to determine whether or not they were inside the edges of the triangular plane. I used the y value at that location on the triangular plane to get the height of the plane. But that location was often closer the back of the court than was the opponent. So the plane would have been higher than at the opponent’s location.

Probably not explaining that last one very well (may decide to add a section showing what was happening). Took quite some time for me to realize I was making, and to sort, those mistakes. All part of the learning process—been many years since I have really messed with geometry.

So I used the actual 3-D values to generate my formulae. x and y values for the two side lines of the plane. y and z values for the height of plane. Got the slope using the rise over run of the two pertinent points. Then used one of the points to sort the intercept. Pretty straightforward stuff. I added a function to help me out.

def get_ln_parameters(x1, y1, x2, y2):
  slp = (y1 - y2) / (x1 - x2)
  y_int = y1 - (x1 * slp)
  return slp, y_int

Interference Or Not

I also decided to write another function to determine whether or not an opponent is under the plane and possibly reaching above it. It takes all the pertinent data: ball position, tin coordinates and opponent’s position, as well as a default value for the center of the court. These are the real 3-D values, not the isometric ones. It uses the above function to determine the necessary formulae. Which it then uses to make its determination. I expect my implementation is a bit sloppy, but it appears to work. I return whether or not the opponent is within the area under the plane and the ratio of the bottom of the plane, at player’s location, and player’s height.

def is_in_pyramid(ball, tin, plyr, cntr=crt_points[24][0]):
  # nbr of points to use when generating values using derived formulae
  nbr_pts = 250
  # going by center of opponent, so need to add 1/2 width went checking whether under/in plane.
  p_wd = 1

  # sides of the triangular plane
  p1_slp, p1_y_int = get_ln_parameters(ball[0], ball[1], tin[0][0], tin[0][1])
  p2_slp, p2_y_int = get_ln_parameters(ball[0], ball[1], tin[1][0], tin[1][1])
  t_tx1 = np.linspace(ball[0], tin[0][0], nbr_pts)
  t_ty1 = (t_tx1 * p1_slp) + p1_y_int
  t_tx2 = np.linspace(ball[0], tin[1][0], nbr_pts)
  t_ty2 = (t_tx2 * p2_slp) + p2_y_int

  # height (z) of plane based on location along side of court (y)
  z_slp, z_y_int = get_ln_parameters(ball[1], ball[2], tin[0][1], tin[0][2])

  is_in_x = False
  ht_over = 20  # ratio of plane height to player height
  t_lim = .1    # given not checking infinite number of points, need to use a little hedge value during comparisons
  for i in range(nbr_pts):
    tmp = False
    if plyr[0] <= cntr:
      tmp = plyr[0] + p_wd >= t_tx1[i] + t_lim and plyr[1] >= t_ty1[i] - t_lim
    else:
      tmp = plyr[0] - p_wd <= t_tx2[i] + t_lim and plyr[1] >= t_ty2[i] - t_lim
    if tmp:
      pyr_base = (z_slp * plyr[1]) + z_y_int
      ht_over = pyr_base / plyr[2]
      print(f"\tplyr x + p_wd: {plyr[0] + p_wd} > {t_tx1[i]} and {plyr[1]} >= {t_ty1[i]}; pyr_base: {pyr_base} ? plyr ht: {plyr[2]}")
      is_in_x = True
    if is_in_x:
      break
 
  return is_in_x, ht_over

If is_in_x is true and ht_over is less than or equal to 1, then there is interference. Otherwise there is not. The plotting of the player above is replaced with the following code.

in_pyr, pyr_z = is_in_pyramid(p_ball, crt_points[16:18], [p_oppo[0], p_oppo[1], p_ht])

# plot opponent and indicating interference if present
pe1 = patches.Arc((o_x_1, o_y_1), 2*iso_fs, iso_fs, color='g')
ax.add_patch(pe1)

if in_pyr and pyr_z <= 1:
  g_x_max, g_y_max = toIsometric2D(p_oppo[0], p_oppo[1], p_ht * pyr_z, _iso_angle)
  ax.plot([o_x_1 - iso_fs, g_x_max - iso_fs], [o_y_1, g_y_max], c='g')
  ax.plot([g_x_max - iso_fs, o_x_2 - iso_fs], [g_y_max, o_y_2], c='r')
  ax.plot([o_x_1 + iso_fs, g_x_max + iso_fs], [o_y_1, g_y_max], c='g')
  ax.plot([g_x_max + iso_fs, o_x_2 + iso_fs], [g_y_max, o_y_2], c='r')
  pe2 = patches.Arc((o_x_2, o_y_2), 2*iso_fs, iso_fs, color='r')
  ax.add_patch(pe2)
else:
  pe2 = patches.Arc((o_x_2, o_y_2), 2*iso_fs, iso_fs, color='g')
  ax.add_patch(pe2)
  ax.plot()
  ax.plot([o_x_1 - iso_fs, o_x_2 - iso_fs], [o_y_1, o_y_2], c='g')
  ax.plot([o_x_1 + iso_fs, o_x_2 + iso_fs], [o_y_1, o_y_2], c='g')

For the current ball height (8.5 feet) and opponent position and height, the image looks just like the one above. The height of the plane at the opponent’s location is 6 feet 4.5 inches above the floor. So approximately 3 inches over the opponent’s head. Might be a tough call for a referee if a let is asked for. But, if the opponent squatted downed or bent over somewhat, then would definitely not be interfering with the striker’s shot.

Let’s lower the ball height to 7.5 feet. Still over the t-line.

isometric view of a hardball doubles court with a ball, resulting 3-D pyramid and symbolized opponent showing interference if player upright
Ball now 7.5 feet above floor, interference present.

Height of triangular plane at opponent’s location is now 5 feet 8 inches. Still might be a tough call. But interference is definitely present if the player is standing upright.

Let’s try moving the ball 4 feet forward.

isometric view of a hardball doubles court with  ball, resulting 3-D pyramid and symbolized opponent showing no interference if player upright
Ball now 7.5 feet above floor and 4 feet in front of t-line,
no interference present.

There is once again roughly 3 inches clearance if opponent upright. In the three cases above, the opponent was under/in the triangular plane. Let’s move the ball 4 feet to the right. So ball is 7.5 feet above the floor, 19 feet from the back wall and 12 feet from the left wall.

isometric view of a hardball doubles court with  ball, resulting 3-D pyramid and symbolized opponent showing no interference if player upright
Ball now at (12, 19, 7.5), opponent not within area
under triangular plane, so no interference.

Done

Change some values and get a new image. Love it when things work as desired. That said if I were to add an isometric projection to a blog post discussing the rules of doubles squash, I would like also add a side view showing the ball, the opponent and the edge of triangular plane.

That’s it for this one. So much fun! May your time at the keyboard be so as well.

Resources