Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/commands/jumbled-tick.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Run one full tick:
1. Check `docs/ISSUE_QUEUE.md` and open GitHub issues/PRs
2. Pick the next actionable issue or in-review PR
3. Dispatch a composer-2.5 subagent as coder or reviewer per the skill
4. If a PR has APPROVE and green CI, merge it
4. If a PR has APPROVE (tests/build/lint verified locally), merge it
5. Update `docs/ISSUE_QUEUE.md`

Report what happened and what to run next.
2 changes: 1 addition & 1 deletion .cursor/skills/jumbled-orchestrator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Coordinate issue-driven development for this repo.
2. Pick the highest-priority issue whose dependencies are **done** (merged PRs)
3. If an issue already has an open PR in review, dispatch **reviewer** instead of coder
4. If reviewer left `REQUEST_CHANGES`, dispatch **fixer** (coder skill, same branch)
5. If reviewer left `APPROVE` and CI green, merge: `gh pr merge <n> --squash --delete-branch`
5. If reviewer left `APPROVE`, merge: `gh pr merge <n> --squash --delete-branch` (verify tests/build/lint locally — no GitHub CI)
6. Update `docs/ISSUE_QUEUE.md` status column

## Dispatch Coder
Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/jumbled-reviewer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ git diff main...HEAD --stat
1. **Scope**: Changes match issue only; no unrelated edits
2. **Correctness**: Logic handles edge cases (odd players, sit-outs, fixed pairs)
3. **Tests**: New behavior has tests if issue requires them
4. **CI**: `yarn test:ci && yarn build && yarn lint` pass locally
4. **Verify locally**: `yarn test:ci && yarn build && yarn lint` pass (no GitHub CI in this repo)
5. **Style**: Matches project conventions

## Severity
Expand Down
35 changes: 0 additions & 35 deletions .github/workflows/ci.yml

This file was deleted.

38 changes: 21 additions & 17 deletions docs/ISSUE_QUEUE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@ Updated by the orchestrator. Status: `open` | `in-progress` | `in-review` | `don

| Priority | Issue | Title | Status | Depends on | PR |
|----------|-------|-------|--------|------------|-----|
| 1 | [#1](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/1) | Project bootstrap and attribution | in-progress | — | — |
| 2 | [#12](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/12) | CI/CD GitHub Actions | in-progress | #1 | — |
| 3 | [#2](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/2) | Fixed pairs data model | in-progress | #1 | — |
| 4 | [#6](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/6) | Enhanced diversity scoring | in-progress | #1 | — |
| 5 | [#7](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/7) | Back-to-back matchup prevention | in-progress | #1 | — |
| 6 | [#3](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/3) | Fixed pairs algorithm | open | #2 | — |
| 7 | [#8](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/8) | Fixed pair sit-out logic | open | #3 | — |
| 8 | [#4](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/4) | Fixed pairs UI — new game | open | #2 | — |
| 9 | [#5](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/5) | Fixed pairs UI — in-game | open | #2 | — |
| 10 | [#9](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/9) | Tests — fixed pairs | open | #3, #8 | — |
| 11 | [#10](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/10) | Tests — diversity | open | #6, #7 | — |
| 12 | [#11](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/11) | Pair visualization polish | open | #4, #5 | — |
| 13 | [#13](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/13) | Site comparison validation | open | all above | — |
| 1 | [#30](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/30) | Richer session mix + instant next round | open | — | — |
| 2 | [#31](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/31) | Edit player names (in-game + setup) | in-review | — | [#37](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/pull/37) |
| 3 | [#32](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/32) | Recent matchup spacing — formal guarantee | open | #30 | — |
| 4 | [#33](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/33) | Skill groups — foundation (data model + UI) | open | — | — |
| 5 | [#34](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/34) | Skill groups — court-to-group mapping + rounds layout | open | #33 | — |
| 6 | [#35](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/35) | Skill groups — separate/combined round generation | open | #30, #33, #34 | — |
| 7 | [#36](https://github.com/javaisbetterthanpython/jumbled-doubles-enhanced/issues/36) | Sit-out modal — draft pre-gen + correct volunteer state | open | #30 | — |

## Parallel batches

- **Batch A** (now): #1, #12, #2, #6, #7
- **Batch B** (after #2 merges): #3, #4, #5
- **Batch C** (after #3/#6/#7): #8, #9, #10, #11
- **Batch D**: #13
- **Batch E** (now): #30, #31, #33 (in parallel)
- **Batch F** (after #30): #32, #36
- **Batch G** (after #33): #34
- **Batch H** (after #30 + #33 + #34): #35

## Upstream mapping

| Upstream (pickleball-shuffler) | Enhanced issue |
|-------------------------------|----------------|
| #4 Improve generation look-ahead | #30 |
| #16 Edit player names | #31 |
| #22 Reduce same person back-to-back | #32 |
| #25 Run multiple groups | #33, #34, #35 |
| #32 Volunteer sit-out modal | #36 |
2 changes: 1 addition & 1 deletion docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Use this document before release to confirm upstream parity and to verify fork-o
| **Partner-pair counts** | Squared penalties via `partnerPairCounts` / `opponentPairCounts` | `src/matching/heuristics.ts` |
| **Variance fairness** | `getVariance()` wired into `getNextBestRound` round selection | `src/matching/variance.tsx` |
| **Tunable search** | `GENERATIONS = 4`, `ROUND_ATTEMPTS = 30`, `ROUND_LOOKAHEAD = 3` | `src/matching/heuristics.ts` |
| **CI** | `yarn test:ci`, `yarn lint`, `yarn build` on push/PR | `.github/workflows/ci.yml` |
| **Local verify** | `yarn test:ci`, `yarn lint`, `yarn build` before merge | Run locally; no GitHub Actions |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Use **issue bodies** as the single source of truth for scope. Keep issues small

- Title: `[#N] Short description`
- Body must include `Closes #N` and a test plan checklist
- CI must pass before merge
- Tests, build, and lint must pass locally before merge (no GitHub Actions CI)
- Reviewer must leave `APPROVE` or `REQUEST_CHANGES` as a PR comment

## Running Multiple Workers
Expand Down
88 changes: 58 additions & 30 deletions pages/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ import {
useShufflerWorker,
} from "../src/useShuffler";
import { ResetPlayersModal } from "../src/ResetPlayersModal";
import { PlayerNameEdit } from "../src/PlayerNameEdit";
import { disambiguateNames, renameWithDisambiguation } from "../src/playerNames";
import { v4 as uuidv4 } from "uuid";

type NamePair = [string, string];
type SetupPlayer = { id: string; name: string };

function getPartner(name: string, pairs: NamePair[]): string | null {
for (const [a, b] of pairs) {
Expand Down Expand Up @@ -76,13 +80,27 @@ function NewGame() {
const [playerInput, setPlayerInput] = useState("");
const playerInputRef = useRef<HTMLTextAreaElement>(null);

const [players, setPlayers] = useState<string[]>(state.players);
const [players, setPlayers] = useState<SetupPlayer[]>([]);
const [courts, setCourts] = useState(state.courts.toString());
const [customizeCourtNames, setCustomizeCourtNames] = useState(false);
const [courtNames, setCourtNames] = useState<string[]>([]);
const [fixedPairs, setFixedPairs] = useState<NamePair[]>([]);
const [linkingPlayer, setLinkingPlayer] = useState<string | null>(null);

const applySetupDisambiguation = (
roster: SetupPlayer[],
before?: SetupPlayer[]
): SetupPlayer[] => {
const names = disambiguateNames(
roster.map((p) => ({ id: p.id, name: p.name })),
before?.map((p) => ({ id: p.id, name: p.name }))
);
return roster.map((p) => ({
...p,
name: names.get(p.id) ?? p.name,
}));
};

const handleAddPlayers = () => {
if (!playerInput) return;
const names = Array.from(
Expand All @@ -93,17 +111,24 @@ function NewGame() {
.filter((x) => !!x)
)
);
setPlayers((players) => [...players, ...names]);
setPlayers((current) => {
const before = current;
const added = names.map((name) => ({ id: uuidv4(), name }));
return applySetupDisambiguation([...current, ...added], before);
});
setPlayerInput("");
playerInputRef.current?.focus();
};

// Load last time's players and court names.
useEffect(() => {
const playerNames = [...state.players]
.map((id) => playersById[id].name)
.sort((a, b) => a.localeCompare(b));
setPlayers(playerNames);
const loaded = [...state.players]
.map((id) => ({
id,
name: playersById[id].name,
}))
.sort((a, b) => a.name.localeCompare(b.name));
setPlayers(loaded);
setCourts(state.courts.toString());

if (state.courtNames.length) {
Expand All @@ -125,7 +150,7 @@ function NewGame() {
}, [state.players, state.courts, state.courtNames, state.fixedPairs, playersById]);

const handleNewGame = async () => {
const names = players;
const names = players.map((p) => p.name);
if (names.length < 4) {
setFormStatus("validating");
return;
Expand Down Expand Up @@ -267,12 +292,13 @@ function NewGame() {
</Button>
</div>
<Spacer y={2} />
{players.map((name, index) => {
{players.map((player, index) => {
const { id, name } = player;
const partner = getPartner(name, fixedPairs);
const paired = partner !== null;
const linking = linkingPlayer === name;
return (
<Fragment key={index}>
<Fragment key={id}>
<div
className={`flex items-center gap-1 rounded-lg px-1 ${
linking
Expand All @@ -289,26 +315,27 @@ function NewGame() {
<span className="text-sm text-gray-500 w-4">
{index + 1}
</span>
<Input
className="flex-1"
aria-label="Player"
value={name}
size="sm"
type="text"
variant="underlined"
onChange={(e) => {
const newName = e.currentTarget.value;
<PlayerNameEdit
name={name}
onSave={(newName) => {
const namesById = renameWithDisambiguation(
players.map((p) => ({ id: p.id, name: p.name })),
id,
newName
);
const oldName = name;
const nextPlayers = players.map((p) => ({
...p,
name: namesById[p.id] ?? p.name,
}));
setFixedPairs(
renameInPairs(fixedPairs, name, newName)
renameInPairs(fixedPairs, oldName, namesById[id] ?? newName)
);
if (linkingPlayer === name) setLinkingPlayer(newName);
setPlayers([
...players.slice(0, index),
newName,
...players.slice(index + 1),
]);
if (linkingPlayer === oldName) {
setLinkingPlayer(namesById[id] ?? newName);
}
setPlayers(nextPlayers);
}}
fullWidth
/>
{paired ? (
<>
Expand Down Expand Up @@ -354,10 +381,11 @@ function NewGame() {
onPress={() => {
setFixedPairs(removePairForPlayer(fixedPairs, name));
if (linkingPlayer === name) setLinkingPlayer(null);
setPlayers((players) => [
...players.slice(0, index),
...players.slice(index + 1),
]);
setPlayers((current) => {
const before = current;
const next = current.filter((p) => p.id !== id);
return applySetupDisambiguation(next, before);
});
}}
>
<Delete />
Expand Down
46 changes: 42 additions & 4 deletions pages/rounds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Pagination,
CardBody,
Divider,
Tooltip,
} from "@nextui-org/react";
import Head from "next/head";
import React, { useEffect, useState } from "react";
Expand Down Expand Up @@ -61,8 +62,34 @@ export default function Rounds() {
const round = state.rounds[displayIndex];
const volunteers = state.volunteerSitoutsByRound[displayIndex];
const { sitOuts = [], matches = [] } = round || {};
const isHistoricalRound = displayIndex < state.rounds.length - 1;

const playerName = (id: string) => {
return state.playersById[id].name;
if (isHistoricalRound && round?.playerNamesById?.[id]) {
return round.playerNamesById[id];
}
return state.playersById[id]?.name ?? "";
};

const renderPlayerName = (id: string) => {
const displayName = playerName(id);
const currentName = state.playersById[id]?.name;
const showTooltip =
isHistoricalRound &&
currentName &&
displayName !== currentName;

if (!showTooltip) {
return displayName;
}

return (
<Tooltip content={`Now: ${currentName}`}>
<span className="cursor-help border-b border-dotted border-neutral-400">
{displayName}
</span>
</Tooltip>
);
};

return (
Expand Down Expand Up @@ -177,7 +204,7 @@ export default function Rounds() {
color="default"
playerId={playerId}
>
{playerName(playerId)}
{renderPlayerName(playerId)}
{volunteers.includes(playerId) ? (
<span className="text-neutral-500 font-semibold text-medium">
{" "}
Expand Down Expand Up @@ -209,11 +236,22 @@ export default function Rounds() {
Court {state.courtNames[index] || index + 1}
</h4>
<div className="text-center">
<TeamBadges team={teamA.map(playerName).sort()} isHome />
<TeamBadges
teamIds={[...teamA].sort((a, b) =>
playerName(a).localeCompare(playerName(b))
)}
isHome
renderName={renderPlayerName}
/>
<Spacer y={5} />
<div className="relative w-full border-b-1 before:px-4 before:-mx-6 before:bg-white before:-translate-y-1/2 before:font-bold before:absolute before:content-['vs']"></div>
<Spacer y={5} />
<TeamBadges team={teamB.map(playerName).sort()} />
<TeamBadges
teamIds={[...teamB].sort((a, b) =>
playerName(a).localeCompare(playerName(b))
)}
renderName={renderPlayerName}
/>
<Spacer y={1} />
</div>
</CardBody>
Expand Down
Loading