Skip to content

Scoring Rewrite - #459

Merged
slamanna212 merged 134 commits into
devfrom
v082
Oct 31, 2026
Merged

slamanna212 merged 134 commits into
devfrom
v082

Conversation

@slamanna212

Copy link
Copy Markdown
Owner

Completely rewrite scoring systems to better support how this app has grown.

Also add initial support for series. Create entire seasons of events with leaderboards and tracking.

slamanna212 and others added 30 commits April 30, 2026 02:06
MetalLB >= 0.13 (annotation-based, recommended):
  service:
    type: LoadBalancer
    annotations:
      metallb.universe.tf/loadBalancerIPs: 192.168.1.100
      # optionally pin to a specific pool:
      # metallb.universe.tf/address-pool: default

  MetalLB < 0.13 (spec-based):
  service:
    type: LoadBalancer
    loadBalancerIP: "192.168.1.100"

  Multiple LB controllers (need to tell MetalLB it's in charge):
  service:
    type: LoadBalancer
    loadBalancerClass: "metallb"

  The externalTrafficPolicy: Local option is also there if you want to preserve client source IPs (common when your ingress controller sits behind the LB).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…st alias

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
slamanna212 and others added 27 commits May 14, 2026 16:16
Adds scoring_type-aware infrastructure across the full stack,
replacing hardcoded blue/red team model with N-team support and
a new Series entity for multi-event championships.

Key changes:
- Migrations 019-022: match_teams, match_game_placements, cumulative-points
  tournament format, backfill, and series tables
- scoring-functions.ts: dual-write placements for Normal/FFA/Position;
  getGameResult() returns typed discriminated union
- match-setup.ts: createMatchTeams(), assignParticipantToTeam(),
  getMatchTeams(), setTeamVoiceChannel(), MAX_VOICE_CHANNELS_PER_MATCH
- transition-handlers.ts: createMatchTeams() on assign transition,
  dual-write voice channel IDs to match_teams
- announcement-helpers.ts: fetchMatchTeamRows() reads from match_teams;
  fetchMatchStartData() uses team names/voice channels from new table
- tournament-bracket.ts: generateCumulativeMatches() + addAllTeamsToMatch()
- standings/leaderboard routes: cumulative-points format support
- Series library + REST API (/api/series)
- AI extractor: scoring_type-aware prompt + placements write-back
- UI: TeamMapScoring/FfaMapScoring components, scoring page dispatcher,
  teamCount/positionScoringOverride in match creation form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Series: full UI at /series (dashboard, create, detail, edit, history);
  navigation entry added with IconTimeline; API already existed from Phase 6
- Setup phase: new src/components/setup/ with setup-phase.tsx dispatcher,
  confirm-participants.tsx, grid-order.tsx, qualifying.tsx, classes.tsx
- Tournament: add 'cumulative-points' to format selector and API validation;
  update TournamentFormat type in shared/types.ts
- CLAUDE.md: document scoring_type system, match_teams, match_game_placements,
  Series system, Phase 8 pending note, updated schema and migration count

All 2215 tests pass; typecheck clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix savePlacementsForNormal to look up match_teams by team_order
  instead of hardcoded backfill IDs (mt_blue_/mt_red_) which don't
  exist for new matches created via createMatchTeams()
- Fix getSeriesStandings to group by user_id instead of participant_id
  so standings aggregate correctly across multiple events for the same
  player; update series-detail-page.tsx to match
- Add tests/unit/lib/match-setup.test.ts: full coverage of createMatchTeams
  (Normal, FFA, idempotency, team_count), assignParticipantToTeam,
  getMatchTeams ordering, and setTeamVoiceChannel
- Add tests/unit/lib/series.test.ts: full coverage of series CRUD,
  addSeriesEvent, listSeriesEvents, removeSeriesEvent, and
  getSeriesStandings (points aggregation, multiplier, ranking)
- Add tests/integration/api/series.test.ts: integration tests for all
  series API endpoints (CRUD, events, standings)
- Add migration assertions for new scoring-rewrite tables and columns
  to tests/integration/database/migrations.test.ts
- Add cumulative-points tournament format test to tournaments.test.ts
- Add match_teams creation tests to transition-handlers.test.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The v082 scoring rewrite built a solid schema and dual-write engine but left
the actual UI and lifecycle disconnected from it: the live assign page was
still the pre-rewrite hardcoded Blue/Red UI, the "new" scoring components
were stub wrappers around the untouched legacy component, and the core
engine hardcoded every write/read to exactly two teams despite match_teams
supporting N.

- scoring-functions.ts: savePlacementsForNormal/getOverallMatchScore/
  determineMatchWinner now handle arbitrary team counts instead of only
  team_order 0/1; Position placements also derive team-level rows for
  team-endurance matches (participants grouped into match_teams)
- result API route now actually uses winnerTeamId/loserTeamId/scores instead
  of silently dropping them and defaulting to team1
- match-setup.ts: team_count/max_teams are now consulted when creating
  match_teams (was always hardcoded to 2); added team-endurance team
  creation that distributes participants across N teams
- voice-channel-manager.ts + Discord bot queue processor: channel creation
  is N-team aware (capped), and the buggy sync loop that collided every
  team beyond order 0 onto the same Discord channel is gone
- assign page now dispatches on scoring_type: Normal renders an N-team
  assignment board (was hardcoded to Reserve/Blue/Red, forcing FFA matches
  through a nonsensical team-assignment UI), FFA/Position render the
  previously-unwired SetupPhase dispatcher
- GET /api/matches/[matchId] now joins game_modes so scoring_type/
  setup_components actually reach the client
- TeamMapScoring/FfaMapScoring are real components now instead of both
  wrapping the untouched legacy SimpleMapScoring; TeamMapScoring renders a
  win-card per match_teams row and submits winnerTeamId

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9hMfCteigpmYvw2v5z4Vk
…ation data loss

Tournaments: the cumulative-points (FFA/Position) standings API returned a
totally different shape than the bracket-format one, but the frontend always
rendered the bracket-format TeamStanding fields — cumulative tournaments
showed a blank standings tab and an empty bracket tab. Added a dedicated
LeaderboardView and gated the Teams/Bracket tabs to bracket formats only.
Also found and fixed a real bug in the cumulative standings query itself:
the status='completed' filter was applied to the wrong JOIN and never
actually excluded pending games' placements from the point total.

tournaments.position_scoring_override was schema that nothing read or wrote
— the documented match → tournament → game fallback chain was actually only
two levels. Wired it into both scoring-functions.ts resolvers and the
tournament create/edit APIs. Also added a cross-check between tournament
format and the mode's scoring_type: previously an FFA/Position mode with a
bracket format either threw an opaque error or silently built a nonsense
1-person bracket.

Migration 021: fixed the inconsistent JOIN types (LEFT for Normal, INNER for
FFA/Position) that could silently default an orphaned-mode FFA/Position
match to Normal — fabricating Blue/Red teams for it while permanently
skipping the migration of its real result data. Every match's scoring_type
is now resolved once (inferring from match_games shape when the mode row is
gone) and used consistently across every backfill section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9hMfCteigpmYvw2v5z4Vk
The scoring rewrite plan called for an ai_processor_version setting so the
scoring_type-aware extraction prompt could be rolled back if it produced
worse results than the old mode-blind one — that setting never existed
anywhere outside the plan doc, so there was no way to roll back a bad
extraction-quality regression short of a code deploy. Added it as a real
stats_settings column, wired into the extractor (legacy forces the original
mode-blind blue/red-only prompt) and exposed via the stats settings page.

scorecard_player_stats.team_id — the new authoritative FK the plan says
replaces team_side — was never written by the extractor; it resolves the
match's actual match_teams row from team_side now, so team_id is populated
for Normal-mode matches going forward. (Went with resolving team_id from the
existing blue/red team_side rather than rewriting the AI prompt to handle
arbitrary N teams: the DM submission flow, review UI, and winner-vote
handler are all blue/red-only throughout, so a partial N-team rewrite of
just the prompt would produce data the rest of that pipeline can't use.)

stat-image-generator.ts had zero scoring_type branching — FFA/Position
matches got a "Team: RED/BLUE" column that's meaningless for them, reading
team_assignment values that don't apply to those modes. Non-Normal matches
now skip that column instead of showing nonsense.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9hMfCteigpmYvw2v5z4Vk
…er race fix

getSeriesStandings ignored series.scoring_config entirely — it summed
match_game_placements.points_awarded directly, a field only Position-mode
scoring ever populates. Every Normal (win/loss) or FFA match added to a
series silently contributed 0 points regardless of who won. Implemented the
documented conversion rules: match_win_points/match_loss_points for
Normal/FFA match events (via the match's overall winner), each match's own
point spread for Position events (position_event_use_event_spread), and
tournament_position_points for tournament events — per-participant rank for
cumulative-points tournaments, champion-only recognition for bracket
tournaments (full bracket placement — runner-up, 3rd/4th — needs dedicated
placement logic beyond this fix's scope). Added a scoring-config-editor to
the series create/edit pages so this is actually reachable from the UI,
which it wasn't before.

Added POST /api/series/[seriesId]/transition with real transition-legality
validation (created → active → complete, cancel from non-terminal states
only) and stripped status from the plain PATCH endpoint, which previously
let status jump to any value with no validation at all.

Fixed a real race in addSeriesEvent: two concurrent calls with no explicit
event_order can both read the same MAX(event_order) before either INSERT
commits, so the loser hit an unhandled UNIQUE constraint 500. It now retries
with a freshly computed order when the conflict is on an auto-assigned
order; an explicitly-requested order that conflicts still errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9hMfCteigpmYvw2v5z4Vk
…2z0a5

Phase 1: Scoring & Match Type System Rewrite
@slamanna212
slamanna212 merged commit 3a1feb7 into dev Oct 31, 2026
1 check passed
@slamanna212
slamanna212 deleted the v082 branch October 31, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants