Deployed
Softball Pulp
Auto-generate your softball lineups using linear programming and PuLP.
Coed softball lineups involve competing constraints: a league-mandated gender ratio in the batting order, individual position preferences, and fair playing time across innings. This stateless Flask app models the problem as two Mixed Integer Linear Programs, solved with PuLP, and produces a batting order and nine-inning fielding schedule from a list of player names, genders, and playable positions.
Tech Stack
- Python (Flask, PuLP, PyPugJS, Gunicorn)
- uv (package manager)
- Pug server-side templating
- Pico CSS 2
- HTMX 2
- SortableJS
- Vanilla JavaScript / localStorage
- Podman
- NGINX
Architecture
- Fully stateless, with no database or user accounts.
- Three-page flow: input player data → drag-and-drop skill ranking → results.
- localStorage persists form state across page navigation.
- Two MILPs are solved per request via PuLP with the CBC solver:
- Batting order (maximizes skill weighting toward the top of the order, subject to gender ratio constraints).
- Fielding assignments (maximizes skill utilization across all nine innings, subject to position preferences, a minimum of four females per inning, and a constraint preventing any player from sitting out two consecutive innings).
- A
/validate-inputsendpoint dry-runs both solvers before the full submission, surfacing infeasible lineups before the page renders. - Containerized with Docker/Podman, served by Gunicorn behind an NGINX reverse proxy on a VPS.
Challenges
- State is passed between pages via localStorage rather than hidden form fields, which would have become unwieldy across three steps.
- PuLP cannot express comparisons between binary decision variables without making the model non-linear. Discouraging unnecessary position changes between innings is approximated by injecting a constant bonus at model-construction time, which is a heuristic rather than a true optimization objective.
- The 3:2 gender ratio constraint applies only to complete groups of five batters. With certain roster sizes, trailing batters fall outside any complete group and go unconstrained, which matches how the league applies the rule.
Learnings
- PuLP with the CBC solver handles 20 players × 9 innings × 10 positions in milliseconds, so the solver is never the bottleneck.
- Expressing lineup rules as LP constraints is more maintainable than a custom heuristic.
- Adding a new rule is additive rather than requiring changes to existing logic.
- HTMX handles the dynamic parts of the input form (adding/removing player cards, pre-submission validation) without pulling in a JavaScript framework.
uvis noticeably faster thanpipfor cold dependency installs in the Docker build.- Documenting even a simple web app is time consuming. AI can scaffold things, but still requires manual rework to keep your own voice.
- How to hand-build SVGs with Figma, so that my softball looks nice.
For the Future
- Move output data into the URL so lineups can be shared directly from the results page.
- Allow configuration before optimization (e.g., all-male roster, adjusted gender ratios, different league rules).
- Lower the minimum player count from 10 to 8 to support smaller rosters.
- Allow a female player to bat multiple times when the roster is short on women.