Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

playoffs

One-screen terminal dashboard for the NHL and NBA playoffs. Stdlib only, single Python file, fits in 80 columns.

Sample output

PLAYOFFS  Thu Jun 11 2026   updated 11:38 AM PDT

RECENT
  18h ago    NBA  R4 G4   SA 106 @ NY 107     NY 3-1

LATER TODAY
  5:00 PM   NHL  R4 G5     VGK @ CAR


Stanley Cup Final (1 active)
  CAR vs VGK      tied 2-2   G5 5:00 PM     VGK·CAR·VGK·CAR

NHL Conference Finals (2 done)
  done: CAR-MTL 4-1 · VGK-COL 4-0

NHL Round 2 (4 done)
  done: CAR-PHI 4-0 · COL-MIN 4-1 · MTL-BUF 4-3 · VGK-ANA 4-2

NHL Round 1 (8 done)
  done: ANA-EDM 4-2 · BUF-BOS 4-2 · CAR-OTT 4-0 · COL-LA 4-0 · MIN-DAL 4-2
        MTL-TB 4-3 · PHI-PIT 4-2 · VGK-UTA 4-2

NBA Finals (1 active)
  NY vs SA        NY 3-1     G5 Jun 13 ★    NY·NY·SA·NY

NBA Conference Finals (2 done)
  done: NY-CLE 4-0 · SA-OKC 4-3

NBA Conference Semifinals (4 done)
  done: CLE-DET 4-3 · NY-PHI 4-0 · OKC-LAL 4-0 · SA-MIN 4-2

NBA Round 1 (8 done)
  done: CLE-TOR 4-3 · DET-ORL 4-3 · LAL-HOU 4-2 · MIN-DEN 4-2 · NY-ATL 4-2
        OKC-PHX 4-0 · PHI-BOS 4-3 · SA-POR 4-1

When live games are happening, a LIVE NOW section appears at the top instead of RECENT, sorted by score gap (closest first) with closeout/stay-alive markers:

LIVE NOW
  NHL  TB 0 @ MTL 0       8:33 - 3rd    G6 ★ closeout       MTL leads 3-2
  NHL  BUF 2 @ BOS 1      End of 2nd    G6 ★ closeout       BUF leads 3-2
  NBA  ORL 88 @ DET 87    4:24 - 4th    G6 ★ closeout       ORL leads 3-2
  NBA  CLE 74 @ TOR 85    3:40 - 3rd    G6 ★ stay alive     CLE leads 3-2

Run

Run it directly — stdlib only, no install step required:

python3 playoffs.py

Tested on Python 3.9+.

To put it on your $PATH, symlink it:

mkdir -p ~/.local/bin
ln -s "$(pwd)/playoffs.py" ~/.local/bin/playoffs

For ambient awareness during a game night:

watch -n 60 playoffs

Sections

Section When shown Contents
LIVE NOW Whenever games are in progress Live games sorted by score gap, with closeout (yellow ★) for series-leader winning, stay-alive (green ★) for trailer winning, G7 marker for 3-3 series
RECENT Only when LIVE NOW is empty Completed games from the most recent slate, found by walking back through final games until a >6h gap. Rows older than 8h are dimmed.
LATER TODAY Today's scheduled games not yet started Time, league, round/game tag, matchup
TOMORROW Tomorrow's scheduled games Same format as LATER TODAY, with "if X wins" tag on G7s contingent on tonight's outcome
Per-league round sections Always Active series with score, next-game info, game-by-game winner sequence; completed series rolled into a single done: line per round

Configuration

Environment variable Effect
NO_COLOR Disable ANSI colors (per no-color.org). Presence of the variable matters, regardless of value.
FORCE_COLOR Force colors even when stdout isn't a TTY. Useful for piping through less -R.

How it works

Pipeline: gather → normalize → group → render.

  1. gather: Two parallel HTTPS requests to ESPN's public scoreboard endpoint, one per league, each using a date-range query covering the entire playoff window. Replaces what would otherwise be ~50 single-day requests.
  2. normalize: Translate ESPN's "Lakers/Rockets" placeholder strings (used for unresolved Round 2+ slots) to "LAL/HOU" style. Once a Round 1 series is decided, resolve the placeholder to the actual winner so duplicate Round 2 entries collapse into one series. All lookups are league-keyed – NHL and NBA share team abbreviations (DAL, MIN, BOS) and nicknames (both leagues have a Kings).
  3. group: Bucket games by (league, frozenset({home, away})) to recover series. Sort each series by date.
  4. render: Print sections with column-aligned formatting. Auto-hide rules (LIVE NOW takes priority over RECENT) make the dashboard adaptive without per-section configuration.

Total runtime: ~500ms.

Design notes

  • Derived state over reported state. ESPN's series.completed flag can lag by hours after the deciding game ends. The dashboard computes done-state from the win count (max(wins) >= 4) instead. When the API and the data disagree, the data wins.
  • Range queries instead of caching. Two range queries replace single-day fetching. Disk caching was prototyped and removed once the underlying volume dropped 24x; the access pattern itself was the optimization.
  • Score-gap sort in LIVE NOW. abs(home_score - away_score) is a direct proxy for "should I tune in?" – a 1-point game with 4 minutes left ranks above a 20-point blowout, regardless of game number or start time.
  • Boundary validation. _is_valid runs twice in gather() – once before normalize (drops TBD vs TBD placeholders) and once after (defends against any same-team edge case introduced by placeholder resolution). Validate at the seam, trust the room.
  • Drops have a witness. Unclassifiable games are dropped by design (play-in games and regular-season leakage carry no usable headline) – but a non-empty headline that fails to classify warns on stderr first, deduped per series stage. A classifier gap once erased the entire conference-finals round from both leagues with no visible symptom; the warning makes the next ESPN vocabulary drift announce itself.

Development

From the repo root:

python3 -m pytest tests/

Tests cover the logic helpers (_safe_int, count_wins, is_series_done, round_num, fmt_state, elim_status_live, normalize_placeholders, find_recent_games, contingent_tag, parse, fmt_when). No network access; all tests use synthetic game dicts.

The suite has caught one real bug that production data was masking: round_num returning 3 for "Conference Semifinals" because "semifinals" contains "final". ESPN happens to use "1st Round" / "2nd Round" naming so the bug was dormant; the test forced the issue.

The failure also flows the other way: the round tests originally asserted invented labels like "Eastern Conference Finals" – which ESPN never sends – so the suite stayed green while every real conference-finals game ("East Final", "West Finals") fell through the classifier and silently vanished from the bracket. The round_num tests now assert the verbatim headline vocabulary captured from the 2024–2026 seasons, and gather() warns on stderr when a non-empty headline fails to classify. Fixtures mirror the wire; drops have a witness.

Files

playoffs/
├── playoffs.py             # main script, stdlib only
├── tests/
│   ├── conftest.py
│   └── test_playoffs.py
├── README.md
├── LICENSE
└── .gitignore

Limitations

  • PLAYOFF_START = date(2026, 4, 15) is hardcoded. Update for future seasons or compute from the calendar.
  • Times use the system's local timezone via datetime.astimezone() and %Z strftime. If your terminal lies about its locale, times will too.
  • No offline mode – a full ESPN outage produces "ESPN unreachable"; a single-league failure still renders the surviving league, under a red "partial view" warning on stdout.
  • WNBA / college basketball / NFL playoffs not supported. Adding them is one entry in the LEAGUES constant plus verification that ESPN uses the same JSON shape.

ESPN endpoint reference

https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard?dates={range}&seasontype=3
  • sport / league: hockey / nhl, basketball / nba
  • dates: YYYYMMDD for one day, YYYYMMDD-YYYYMMDD for a range
  • seasontype=3: post-season (excludes regular season and exhibitions)

Per-event round and game number live at competitions[0].notes[0].headline (e.g., "East 1st Round - Game 6"). Series state lives at competitions[0].series.summary but the score-derived computation is more reliable.

Observed headline vocabulary (verbatim, stable across the 2024–2026 seasons):

Round NHL NBA
1 East/West 1st Round - Game N East/West 1st Round - Game N
2 East/West 2nd Round - Game N East/West Semifinals - Game N
3 East/West Final - Game N East/West Finals - Game N
4 Stanley Cup Final - Game N NBA Finals - Game N

Note that "Conference" never appears. Unplayed deciding games carry an If Necessary suffix. Known drops (classified to round 0, no warning): NBA Play-In - ... labels, and the empty headlines on regular-season games that leak into the date window.

License

MIT. See LICENSE.

About

One-screen terminal dashboard for the NHL and NBA playoffs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages