import logging
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Ellipse, FancyArrowPatch, Rectangle
from gridiron import Play, Player, defense, style, draw_marker
from gridiron.field import HASH_NEAR, HASH_FAR
from gridiron.formations import LINE_D, TE_W, WING_W, OL_SPLIT
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
style.apply()
ELIG = style.THIRD # aqua ring: an eligible receiver
FOUL = "#e34948" # red ring: the problem (ineligible, or illegal)
TE_RING = style.LINE_TO_GAIN # gold ring: a tight end (as in Personnel)
OL_RING = "#4a3aa7" # purple ring: a lineman (as in Personnel)
UC, PIS, GUN, TB_D = -1.9, -4.0, -5.0, -7.0 # QB and back depths, yards behind the line
WIDE, SLOT = 15.0, 9.0 # typical receiver widths from the ball
# Real "off the line" is about a yard deeper than "on the line", less than one marker wide. This chapter's
# formation plates draw off-the-line players deeper (OFF_D) and add a dashed guide, so on/off can be SEEN.
OFF_D = -2.7
GUIDE_D = -1.75 # where the dashed "a yard back" guide is drawn (between the two depths)
def line5():
"""The five offensive linemen; only the center is lettered."""
return [Player(k, k, "off", LINE_D, w, label=("C" if k == "C" else ""))
for k, w in [("LT", -2 * OL_SPLIT), ("LG", -OL_SPLIT), ("C", 0.0),
("RG", OL_SPLIT), ("RT", 2 * OL_SPLIT)]]
def P(key, pos, d, w, label=None):
"""An offensive skill player at (d, w): d = yards from the line (negative = backfield),
w = yards to the offense's right of the ball."""
return Player(key, pos, "off", d, w, label=label or key)
def D(key, pos, d, w, label=None):
return Player(key, pos, "def", d, w, label=label)
def hide_yard_numbers(ax):
"""Drop the faint painted yard numbers (they sit where receivers stand in these zoomed pictures)."""
for t in list(ax.texts):
if t.get_text().isdigit() and t.get_alpha() is not None and abs(t.get_alpha() - 0.55) < 1e-6:
t.remove()
def say(fld, p, d, w, text, color=None, ha="center", va="center", fs=7.2, weight="normal", box=True,
italic=False):
"""A short label at alignment coordinates (d, w), on a light backing so it reads over field lines."""
x, y = fld.xy(*p.to_abs(d, w))
bb = dict(boxstyle="round,pad=0.22", fc=style.SURFACE, ec="none", alpha=0.9) if box else None
return fld.ax.text(float(x), float(y), text, fontsize=fs, color=color or style.INK, ha=ha, va=va,
fontweight=weight, zorder=12, linespacing=1.15, bbox=bb,
style="italic" if italic else "normal")
def leader(fld, p, frm, to, color=None, lw=0.6):
"""Thin leader line from a label at frm=(d, w) to a player at to=(d, w)."""
(x0, y0), (x1, y1) = fld.xy(*p.to_abs(*frm)), fld.xy(*p.to_abs(*to))
fld.ax.plot([float(x0), float(x1)], [float(y0), float(y1)], color=color or style.INK_2, lw=lw, zorder=6)
def on_line(p):
return [k for k, q in p.players.items() if q.side == "off" and q.d > -1.0]
def count_line(fld, p, keys, dy=2.9):
"""Number the players on the line, left to right, just in front of each (on a light backing)."""
for i, k in enumerate(sorted(keys, key=lambda k: p.players[k].w), 1):
pl = p.players[k]
x, y = fld.xy(*p.to_abs(pl.d + dy, pl.w))
fld.ax.text(float(x), float(y), str(i), fontsize=6.6, ha="center", va="center", color=style.INK,
fontweight="bold", zorder=12,
bbox=dict(boxstyle="circle,pad=0.12", fc=style.SURFACE, ec="none", alpha=0.95))
def depth_guide(fld, p, w0, w1, label_w=None, text="a yard back (depth stretched)"):
"""Dashed guide between 'on the line' and 'a yard off it', from width w0 to w1, with a small label."""
(x0, y0), (x1, y1) = fld.xy(*p.to_abs(GUIDE_D, w0)), fld.xy(*p.to_abs(GUIDE_D, w1))
fld.ax.plot([float(x0), float(x1)], [float(y0), float(y1)], color=style.INK_2, lw=0.7,
ls=(0, (3, 3)), zorder=2.5, alpha=0.8)
if label_w is not None:
say(fld, p, GUIDE_D - 0.95, label_w, text, fs=5.8, color=style.INK_2, italic=True, box=False)
def shade_side(fld, p, w0, w1, color, alpha=0.10):
"""Tint the field between widths w0 and w1 (alignment yards) over the whole drawn depth."""
(xa, _), (xb, _) = fld.xy(*p.to_abs(0, w0)), fld.xy(*p.to_abs(0, w1))
lo, hi = sorted([float(xa), float(xb)])
y0, y1 = fld.ax.get_ylim()
fld.ax.add_patch(Rectangle((lo, y0 + 0.5), hi - lo, (y1 - y0) - 1.0, fc=color, ec="none", alpha=alpha,
zorder=0.5))
def numbers_band(fld, p, sideline_w, alpha=0.22):
"""Shade the painted-numbers lane, 12 to 14 yards in from the sideline at width sideline_w."""
s = 1 if sideline_w > 0 else -1
shade_side(fld, p, sideline_w - s * 12.0, sideline_w - s * 14.0, style.MUTED, alpha=alpha)
def vdim(fld, p, w, d0, d1, text, fs=6.6):
"""A vertical dimension arrow at width w from depth d0 to d1, labelled on its right."""
(x0, y0), (x1, y1) = fld.xy(*p.to_abs(d0, w)), fld.xy(*p.to_abs(d1, w))
fld.ax.add_patch(FancyArrowPatch((float(x0), float(y0)), (float(x1), float(y1)), arrowstyle="<|-|>",
mutation_scale=7, color=style.INK_2, lw=0.8, zorder=6))
xm, ym = fld.xy(*p.to_abs((d0 + d1) / 2, w + 0.35))
fld.ax.text(float(xm), float(ym), text, fontsize=fs, color=style.INK, zorder=12, ha="left", va="center")
def hdim(fld, p, d, w0, w1, text, fs=6.3):
"""A horizontal dimension arrow at depth d from width w0 to w1, labelled just below it."""
(x0, y0), (x1, y1) = fld.xy(*p.to_abs(d, w0)), fld.xy(*p.to_abs(d, w1))
fld.ax.add_patch(FancyArrowPatch((float(x0), float(y0)), (float(x1), float(y1)), arrowstyle="<|-|>",
mutation_scale=7, color=style.INK_2, lw=0.8, zorder=6))
say(fld, p, d - 0.75, (w0 + w1) / 2, text, fs=fs)
def referee(fld, p, d, w):
"""The referee: a white, striped disc (officials are neither offense nor defense)."""
x, y = fld.xy(*p.to_abs(d, w))
fld.ax.add_patch(Circle((float(x), float(y)), 0.8, fc="white", ec=style.INK, lw=0.9, hatch="||||", zorder=9))
def pro_11(**moves):
"""11 personnel, under center, 2x2: X on the line left, H in the left slot, Y in-line right,
Z off the line right, the back 7 yards deep. moves = {key: dict(d=..., w=...)}."""
off = line5() + [P("QB", "QB", UC, 0, "Q"), P("RB", "RB", TB_D, 0, "R"), P("Y", "TE", LINE_D, TE_W),
P("X", "WR", LINE_D, -WIDE), P("H", "WR", OFF_D, -SLOT), P("Z", "WR", OFF_D, WIDE)]
return [q.moved(**moves[q.key]) if q.key in moves else q for q in off]
def i_form_21():
"""21 personnel, I-formation: X on the line left, Y in-line right, Z off the line right,
the fullback (F) and the tailback (R) stacked behind the quarterback."""
return line5() + [P("QB", "QB", UC, 0, "Q"), P("FB", "FB", -4.4, 0, "F"), P("RB", "RB", TB_D, 0, "R"),
P("Y", "TE", LINE_D, TE_W), P("X", "WR", LINE_D, -WIDE), P("Z", "WR", OFF_D, WIDE)]