Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/jjdunlop/reds-social-league/archive/refs/heads/main.zipWordPress plugin for MURVC's social volleyball league: registration and
rego, teams and invites, free agents and placement, courts and game
nights, automatic fixtures, results, ratings and ladders, referees and a
live scoreboard, and team balances. Still to come: the sub list and
finals (see CLAUDE.md).
A season preview while the ladder is empty, and three small fixes (0.18.0)
- Season preview. Until a season's first fixtures are published, the League Teams, League Ladder and Fixtures & Results pages show a preview card: when the season starts ("2027 Pre-season (Summer) starts Sunday 24 January") with a countdown ("53 days to go", counted again in the visitor's browser by the site's own date and time zone, so a cached page never shows an old number and every visitor sees the same count), how many teams are in so far in each division (never their names, never applications still waiting), the Ranked tiers from Bronze to Diamond (three placement games, then climb) and what Unranked is, the free agent dates while the window is still to close, and buttons to register and to read how the league works. It goes as soon as a night's fixtures are published or the first night has passed. A season with no dates says its dates are coming soon. On the Teams page it sits above the team list (in place of "No teams yet"), on the Ladder above "How the ladder works", on Fixtures & Results above the weeks.
- How the league works button. Social League → Teams has a League pages box: pick the page that explains the league and the preview links to it. With none picked there's no button.
- League pages box note. If the chosen page is later unpublished, the box says so under the picker (the button is hidden until it is published again).
- Fixes: the menu's "Register for YEAR" names the year of the season open for sign-up (or, with none open, of the season after, never a year already gone) when the sign-up season it was pinned to has finished; with nothing open for sign-up, a team application or a free agent entry (the older "looking" form too) is refused with "Sign-ups for SEASON have closed and nothing is open yet." (and the league page says so where those forms were, and its Not yet words and messages offer only an invite code) instead of going into a finished season; a season rego that still counts after its season's dates moved later covers the player to the new last night on the league page, so the "runs out soon" note judges that night (and stays quiet when the next season is already paid for).
- Dev:
RSL_Public::season_preview()andpreview_state()(the one show rule; both take the season's nights when the caller has them),preview_fa_words(),teams_in_words(),countdown_words(),days_until(),count_word(),about_url()andABOUT_OPTION(rsl_about_page_id);RSL_Seasons::closed_words()(errorrsl_signup_closed;signup_choices()no longer falls back to a finished season);RSL_Portal::menu_season();RSL_Rego::paid_ahead()counts a season rego for its whole season;dev/tests/test-season-preview.php.
Start a team in one step, pick the season, and a menu that knows you (0.17.0)
- Start a team at registration in one go. Choosing Start a team when registering shows the team's details right there: which season, team name, division, roughly what level and colour. One Register sends the registration and the captain application together: "You're registered and your application for TEAM is in. Once the league approves it you'll get an invite link for your players."
- If the application can't go in (no name, a name already taken that season, already on a team that season…), the registration still stands and the page says why. Where sending it again can work, it also opens the team application with what was typed. Pressing Register twice never reports an error for an application that went in.
- Pick the season. Find me a team (when registering and on the league page) and the captain application ask which season, from the seasons open for sign-up with their dates; the season signing up now is chosen already. Joining with an invite code keeps the team's own season.
- The league page shows what you've signed up for in a later season too (your team, your application waiting, or your place on the free agent list) under Later season, and the season choices leave out seasons you're already in. A later season's team's invite link works as soon as the team is approved.
- The league page's menu item follows the player (logged in only): Register for 2027 (the year of the season signing up now) until they've registered, My team once they're on a team or have a team application waiting, My league when they're registered without a team. Logged-out visitors see the menu as it's set up, and so does a menu item that heads a dropdown.
- Admin season lists (Teams, Free Agents, Courts & Nights, Add a rego by hand) now run from this year to two years ahead.
- Fixes: rego notes are cut on a whole character and always keep the newest reason, so a cancelled order paid again gets its rego back even when the note was long; adding a 12-month rego by hand says so only once it's saved, with its dates; the "runs out soon" note doesn't show when the next season is already paid for; a free agent waiting for a team who can't pay online is told why and to contact the league; a team that couldn't be saved is reported as an error, never as sent; a free agent form from a season the page no longer shows asks for a reload instead of changing another season's entry; team balance notes (top-ups, admin adjustments, carried balances) are cut on a whole character too.
- Dev:
RSL_Seasons::signup_choices()andpick_open()(the one check of a season a player picks) anddefault_pick()(the season a picker starts on);RSL_Portal::season_field(),team_fields(),apply_from_post(),apply_retry()and the apply draft (APPLY_DRAFT_PREFIX, a minute, like the flash);RSL_Portal::menu_labels()onwp_nav_menu_objects(menu_state(),flush_menu_state(); a dropdown heading keeps its words);RSL_Teams::joinable_seasons()includes the later seasons open for sign-up;RSL_Teams::create()reports a team it couldn't save;RSL_Rego::paid_ahead();RSL_Rego::note_fit_to()(public, for ledger notes too);dev/lib-mail-stop.php, used by every test that makes accounts and by the fake-data scripts; every test that makes accounts cleans up in afinallyblock, so a check that throws partway still removes its accounts and rows;dev/tests/test-menu-label.php.
League emails in the club look (0.16.0)
- Every league email now has the club's email look, the same as the Club Manager's: the club's logo and name, REDS SOCIAL LEAGUE above the heading, the subject as the heading, the message, the club footer, and a plain-text version. That covers the free agent list, invites, requests, placements, team approvals, email invites and the "can't make it" notices to the league.
- The main link is a button ("Your league page", "Pay your rego"); invite links stay visible so captains can copy them; other links stay links.
- Sender: the Club Manager's sender (VVL Oversight → Emails → Sender); the league never sets its own. Replies go to the league contact emails (Team Balances → Rules) when they're set, so every player sees those addresses: use a league address there, not a personal one.
- Without the Club Manager, emails go out as plain text exactly as before.
- Nothing already queued changes: the outbox stores plain text and the look is added when it's sent. A send that fails, or throws, is recorded on its outbox row and tried again like any failure.
- The league page's "runs out soon" note says a yearly rego starts when the current one ends only when it would: the rego on the card is running today. For cover that starts later it just offers the yearly rego.
- While the plugin updates its tables after an update (a few seconds), regos still count by their own dates, so nobody covered is asked to pay in that moment.
- Dev:
RSL_Mail::send_now()is the one send path (the club look when the Club Manager'sTeamOversight_Emailis there, else plainwp_mail());club_email()is the pure formatter; the filterrsl_mail_club_lookcan only switch the look off (tests use it);dev/tests/test-mail.phpcovers both paths.RSL_Database::schema_current(): the rego reads leave out the schema 11 columns until the upgrade has finished. The tests' cleanups keep their mail stop on until the throwaway accounts are deleted (and turn off Ultimate Member's account deleted email), andtest-registration.phpholds the league send run off and parks its own queued mail, so nothing a test queues goes out from another request.
Season rego or yearly rego, the player picks the season (0.15.0)
- Players can pay for one season or for a year. The league page offers whichever the league has set up, season first: "Pay the $15 season rego" with a choice of season, and "Pay the $40 yearly rego" (prices from the shop).
- The player chooses the season from the seasons open for sign-up, each shown with its dates, e.g. "2027 Pre-season (24 Jan to 21 Mar)": the season people are signing up for, plus later seasons whose dates are set. Finished seasons are never offered. The season shows in the cart and on the order.
- A season rego covers that season only: from its first game night (or the payment date, if later) to its last. A season with no game nights set yet covers its quarter (January to March, and so on), and says so. If the league moves the season's nights later on, a rego paid for it still counts for that season.
- Already covered for every game night of a season (by a rego or a full or life membership)? That season can't be added to the cart, so nobody pays twice.
- A yearly rego is unchanged: 12 months from payment, or from when the current rego ends.
- The league page says what covers you ("Season rego, 2027 Pre-season, until 21 Mar", "Yearly rego until 2 Nov 2027", "Club membership until …"), counts a rego paid ahead for the season you're playing in (and any cover that starts before that season does), and 30 days before a rego runs out offers the next one. Captains still see only ✓ / ✗, judged for their team's season.
- Emails that mention the rego say "$15 a season or $40 a year" and link to the league page (straight to checkout when only the yearly rego is on offer).
- One rego in the cart at a time: choosing another season, or the other kind, swaps it (also when a saved cart comes back at login).
- Between seasons, or while next season's sign-ups overlap this season, the league page asks about the season you play in next, and never sells a season rego that wouldn't clear you for it.
- Refunds, cancellations, an order paid again (the rego comes back with the same end) and an admin's Remove work the same for both kinds. A season rego refunded after its season has ended stays ended, even if the season's last night is moved later afterwards.
- A rego payment that can't be saved (rare: a moment while the plugin updates its tables) leaves a note on the order; Pick up rego purchases on Social League → Registration then gives it.
- Admin (Social League → Registration): a Season rego product beside the yearly one (both optional; they can't be the same product, and neither can be the team top-up product); Add a rego by hand takes 12 months or a season (any season this year or next, finished ones too, to record a payment made another way); the regos list shows the season.
- Dev:
rsl_regosgainsseason('' = 12 months) andgranted_end(schema 11);RSL_Seasons::open_for_signup();dev/tests/test-seasons-open.php.
Duplicate accounts: an account in use in the league isn't merged away (0.14.0)
- The Club Manager's merge tool (Duplicate accounts) refuses to merge away an account that's in use in the league. The preview lists what it's in (registered as a player, on a team or the free agent list in a season, an invite (one a captain typed to its email address included) or a request to join, a social rego, the referees and court supervisors list or a job, a team balance line or top-up order, league email, the placement draft, or something it did as a captain, ref or admin) and says to keep that account instead and merge the other one into it.
- The account kept can be in use: its league records stay exactly as they are. When both accounts are in use, the preview says so and that they can't be merged.
- A merge never changes a league record: nothing moves, and nothing happens on Undo. What the league reads from the account itself (date of birth, name, membership) follows the Club Manager's merge, as it does everywhere else. The same check runs again inside the merge, so something that changed since the preview still stops it, and a check that fails stops the merge rather than letting it through.
- Dev:
RSL_Merge::columns()is the one list of columns that name an account;dev/tests/test-merge.phptries a row in each, and fails if auser_id/*_bycolumn in the schema, or any otherbigintcolumn not on its list of ids that aren't accounts, isn't on the list.
Club profile first, and ages from it (0.13.0)
- Registering needs a complete club profile, the same details as a VVL
trial application: first and last name, mobile, date of birth and gender
(
RSL_Players::profile_fields()). The league reads them from the player's account and never keeps a copy. The register page shows them back ("Your details — from your club profile") with an Edit your profile link; if any are missing (or the date of birth isn't a real date) it lists what's missing and links to the profile instead of showing the form, andRSL_Players::register()refuses a new registration without them. An invite link is remembered while they fill it in. Updating your experience answers later doesn't ask again. - Nobody is asked whether they're under 18. It comes from the date of
birth (
RSL_Players::is_under18()), judged on the earlier of today and the season's first night: 17 while captains are browsing counts, and so does 17 on the first night, for the whole season. An account with no date of birth counts as under 18 (the safe side). Under-18s are still left out of the list captains browse (unless an admin allows it), and admins see each free agent's age (or No date of birth). - The free agent row's
under18column is now only an admin's Treat as under 18 tick (Free Agents, for an account whose date of birth is wrong); players can't set it, and it adds to the date of birth rather than replacing it (RSL_Free_Agents::under18_for()). Ticks saved by players before 0.13.0 count as admin ticks. - Dev:
dev/lib-profile.phpgives pretend accounts a profile (rsl_dev_profile(),rsl_dev_dob()); the tests and both fake-league scripts use it.
Players without a team: free agents and placement (0.12.0)
- Sign-up season.
RSL_Seasons::signup()is the season people join, apply for and go on the free agent list for. It's the current season until next season's first night is set and its team-building window has opened — then it's next season, while this one finishes. An admin can pin it. The league page shows "This season" and "Next season" while both run. - Four routes at registration: join a team I know (invite link or code), start a team (captain application), find me a team (the free agent list), or not yet.
- The free agent list (
rsl_free_agents, one row per person per season): division (Unranked by default for beginners; Ranked or Either is the player's consent to Ranked), earlier/later games, weeks away, a friend group (up to 3, joined with a shareable code — never looked up by email), happy to captain, under 18 (admin-only; from 0.13.0 worked out from the date of birth), let captains find me. - The team-building window (settings, counted back from night 1): the list opens 21 days before; until the deadline (10 days before) captains with open spots ("Looking for players?") can browse the list and invite (the player accepts), and free agents can ask up to 3 teams (only that team's captain can say yes; requests expire after 3 days and never past the deadline; "not this time" is final for that team that season).
- What captains see: first name, level, positions and "+N friends" —
built in one place (
RSL_Free_Agents::browse_for_captain()), never emails, surnames, ages, the under-18 box, weeks away or bands. Under-18s are left out of browsing (unless an admin turns that on), though an under-18 can still ask a team themselves. No free text from players. When someone joins through the league (pool invite, request, placement, admin), the player and their captain are emailed each other's name and email; rosters still show names only. - Placement (
RSL_Placement, pure core like the pairing engine): after the deadline it fills short teams first (under 7), tops up teams that asked (to 8), then forms new teams of about 8 from the rest — grouped by level and game time, friend groups never split, a volunteer captain in each, Unranked-only players never in Ranked, nobody over 10. The admin reviews it on Social League → Free Agents → Placement, moves anyone (whole groups move together; a stale tab is refused), names new teams, and confirms (safe to run twice). Anyone not placed is told why, charged nothing, and first in line. Placed players get 3 days to say "Not for me". - One team per person per season holds on every route: joining runs under a per-person database lock; the roster maximum (10) applies to every route except an admin's, under a per-team lock so two people joining at once can't push a team over it. Joining by any route marks the free agent placed and closes their other requests and invites.
- Email queue (
rsl_outbox,RSL_Mail): free-agent emails are queued with a dedupe key and sent in batches of 25 by cron (rsl_hourly,rsl_send_outbox); sent mail is deleted after 30 days. The Free Agents screen shows queued/failed mail and can send now or retry. - Money: registering, the list and being placed are free; the rego is needed before a first game. Team cards offer "Pay my share": each player's fair part of what the rest of the season still needs, less what they have already topped up, so it comes off as they pay and together the shares cover the season.
Review fixes (0.12.0)
A five-lens review (scalability, security, privacy, correctness, robustness) with skeptical verification of every finding kept 46 issues; they are fixed (or found not to be real) and each fix has a test. What changed for people:
- Money.
RSL_Payments::sync_night()is the one place game fees and late fees are worked out, under a per-night lock, and every row carries a unique charge key — two overlapping publishes can't double-charge. Late fees are per game (a late double-header withdrawal is $300). Only the first team out of a game pays: pulling out of a game the other team had already left costs nothing. A game that was played after a "can't make it" is charged normally to both teams; a forfeit by the withdrawing team never charges its opponent. A captain can't withdraw a "can't make it" once the night has started or a result is in; admins can (also for handled ones). Publishing refuses while a team with a week off still has a game in the draft; a night with results, or one that has already started, can't be unpublished (an admin can still unpublish a played night as called off, which takes its fees off). - Refunds. WooCommerce refunds and cancelled orders reverse top-ups (capped at what was credited) and end the rego bought in that order; a cancelled order set back to Processing or Completed puts the top-up back (less any refunds) and gives the rego back (not one an admin removed); admins can remove a rego by hand. The rego is sold one at a time.
- Seasons. Admins carry a team's balance to next season (paired rows); earlier seasons' non-zero balances stay listed until cleared. A "League contact email(s)" setting (Team Balances) gets the league's notices.
- Calendar. Re-saving season dates only removes empty nights; a night with fixtures, results, ref jobs, "can't make it" requests or charges is kept and marked until it's cleared on the Fixtures page; week-off and two-games requests on a removed night go with it (the save notice names those teams). Making a night a Bye also withdraws any leftover "can't make it". A night with games can't be made a Bye or Finals. Both ask before changing a published night.
- Refs. Refs can't take a game once it has finished (supervisors: a venue once its last game ends) — admins assign those. A result or forfeit the league enters is locked to the ref, who can correct only their own save. Removing a ref or supervisor releases their jobs still to come; a venue's supervisor goes with its last game that night, but remaking a night's fixtures keeps the supervisor of a venue that still has games. The pay report CSV is safe to open in a spreadsheet. Scoreboard Undo survives a reload.
- Public pages. The season's stats are cached between views (keyed by a one-query fingerprint of the data, so they're never stale), "hype" no longer runs a query per game, live scores are polled only during the night's games (from 15 minutes before to 30 after, slower when nothing changes, stopping when every result is in). No "Even match", upset or giant-killer percentage while a team is in placements (it would leak the hidden starting level). Ratings and ladders count published nights only. "Can't make it" shows "Free until …" or the charge before confirming.
- Teams. Last season's invite links stop working; an email invite gets the same answer whether or not the email is registered (unregistered emails wait for the person to register); captains get 20 email invites a day; withdrawn and declined applications free the team name; players can leave a team; admins can use the captain buttons.
- Housekeeping. Options read on every request are autoloaded; only one
request runs a schema upgrade at a time; timestamps are written in the
site's time; the dev scripts and tests refuse to run on a production site
unless
RSL_DEV=1is set, and are left out of release downloads (.gitattributes).
Team balances and payments (0.11.0)
- Each team has a balance (
rsl_ledger: money in positive, charges negative). Game fees ($75 incl. GST by default) are charged when a week's fixtures are published — per game, so a double-header is two. The charges follow the published fixture: unpublish and they come off; add or remove a game and they follow. Teams are encouraged to pay before Monday. - Not fixtured when owing more than $75 (setting): one forgotten week is fine, two isn't. The fixtures engine leaves the team out ("Owes $150 — not fixtured until paid"), records it so it counts like a week off (no bye points, no finals round), and the team page says so.
- "Can't make it" after fixtures are out: before the cut-off (48 hours before the night by default) neither team in that game pays; after it the withdrawing team pays both fees ($150) and the opponent pays nothing. "We can make it after all" undoes it. Admins see free/late on the night.
- Top-ups: any player on the team pays any amount on the team page
(what's owing, next game, rest of the season, or other) through the
club's WooCommerce checkout — a placeholder product (Published, catalog
Hidden) whose price is set from the amount, GST included whatever the
shop's tax setting. Each paid order item is credited once (UNIQUE
order_item_id). The captain sees who has paid what. - Admin, Social League → Team Balances: every team's balance (owed / credit totals), each team's history, credits and charges with a note (cash, refunds, corrections), and the rules (fee, cut-off, owing limit, top-up product).
- Team balance is private to the team's players and admins.
Referees, court supervisors and the live scoreboard (0.10.0)
- Who: players tick Happy to be a paid referee when they register (or in My details); an admin approves them on Social League → Refs & Supervisors. Court supervisors are added there by email (one per venue per night: questions, first aid, keeping the night running).
- Ref Jobs page (auto-created): once a week's fixtures are published, approved refs take games one at a time — so a night can be ref, ref, play, ref — and supervisors take their venue. Each person sees their night as a timeline, the open jobs (with the reason when one isn't possible), and their pay so far and booked ahead. Checks: never your own team's game, never two places at once, 45 minutes to get between venues, a cap on games per night, a supervisor stays at their venue. Refs can drop a job until a cut-off before the night.
- Pay: flat rates (default $25 a game refereed, $30 a night supervised), recorded on each job when it's taken. Pay report for any dates (nights that have happened) with a CSV for the treasurer; admins remove no-shows on the Fixtures page, which also shows each game's ref and each venue's supervisor with quick Assign.
- Live scoreboard for the ref's games (on the night and two days after): tap a team for a point, undo, swap sides, sets end at 25 by two (third set 15 or 25), Time's up ends a set as it stands, Save result records it (or type it in). Every tap is kept on the phone first, so slow or no signal never loses a point or a reload; the score so far is sent quietly after each point (whole score + a time-based sequence, so a late update never overwrites a newer one). One scoreboard open at a time; it closes after saving.
- Live on the public Fixtures page: games in progress show LIVE with the sets so far and the current set; during the night's games the page updates itself about every 20 seconds (no reload) — see 0.12.0.
- Tables
rsl_officials,rsl_jobs;games.live(schema 8). ClassesRSL_Officials,RSL_Ref_Page,RSL_Admin_Officials.
Team season schedule and night requests (0.9.0)
- Season schedule on every team page: each night of the season — its games and results once published, TBA ("fixtures out Mon 26 Oct") before, bye weeks and finals.
- Captain requests (buttons on the schedule; captains and admins only):
- Week off — before that week's fixtures are out; free; the fixtures leave the team out. Not a bye: no turn-up points, no finals round.
- Two games — before fixtures are out; first in line for a spare-slot double-header (a wider gap is accepted than for season-long opt-ins; never a third meeting).
- Can't make it — after fixtures are out; emails the league admin and shows on that night in Social League → Fixtures with Mark handled (the automatic replacement offer comes later). One live request per team per night; week off / two games can be withdrawn until fixtures are out. Requests are visible only to the team's members and admins.
- Table
rsl_requests(schema 6), classRSL_Requests.
Fair however many games: placements, finals eligibility (0.8.1)
- Ratings don't depend on how many teams there are — they come only from games played (checked: a strong team joining changes no one's rating; beating two teams moves those two and others by at most a few points). Ladder places do, so teams in placements aren't numbered: they're listed under the settled teams and take their place after 3 games, instead of pushing everyone down after one night. Their rating number isn't shown either.
- Season Points: +10 for turning up is once per night (double-headers don't double it); a week the fixtures leave a team out (odd numbers) still earns it. Wins, close games and upsets still count per game.
- Finals eligibility (Ranked): a team needs 5 regular-season
rounds (nights, not games — double-headers don't fast-track),
counting weeks the fixtures left it out but not forfeits. Ladder
column: ✓, "2 to go", or blank once it can't get there. Setting:
option
rsl_finals_min_rounds(filter of the same name). - The Ranked ladder no longer has a Level column (rating and tier tell that story); Unranked keeps Level; every team page shows it.
Fair progression: earned tiers, Season Points, hype not odds (0.8.0)
The rating stays honest (it orders the Ranked ladder and makes the fixtures, and it can go down). What a team earns is designed to feel fair, the way ranked modes in games work:
- Earned tiers (middle ground): promotion is instant; demotion only
after 2 rounds in a row more than 50 points below the tier's floor
(the first such round shows "At risk"), and one tier at a time; a team
that reaches Silver keeps at least Silver all season (and never
shows at risk there). Team pages show a progress bar to the next tier.
Rules in
RSL_Stats::step_tier(); numbers are constants. - Season Points and levels (both divisions — Unranked teams get progression too): +10 per game played, +5 win, +3 draw, +3 close game (third set, 1–1, or within 6 points overall) win or lose, +5 upset win, +10 per achievement. Only ever go up; every 40 points is a level. Level on the ladder, level + progress bar on the team page.
- Close one tag on close results; Promoted achievement.
- Season leaders on the ladder: Most improved, Giant killers, top Season Points per division.
- Hype, not odds, on coming games: Top-of-table clash, Even match,
Rematch (with last time's result), First meeting, win streak on the
line. No public "30% chance" before a game; Upset! still celebrates
it afterwards. Set chances stay on the admin page
(
RSL_Public_Fixtures::SHOW_SET_CHANCESturns them back on publicly).
Timed-game ratings, set chances and game-style pages (0.7.0)
Ratings for timed games. Games are best of three inside an hour: a set
can end when time's up (the score as it stands counts as a set win), a
third set may go to 15 or just run to time, and a game can end 1–1 (a
draw on the record). Ratings now use the points in every set (each
rally a small contest, discounted because rallies aren't independent)
plus a small bonus for winning sets, with sets counted in proportion
to how complete they were. A 1–1 draw where one team won far more points
still moves the ratings. dev/simulate-ratings.php plays timed seasons
rally by rally: points + set bonus finds the true order faster than sets
alone (after 2 rounds about as well as sets alone after 4) and is closer
at season's end (48 vs 53 rating points off). Uncertainty is calibrated
from the same simulations. A team is in placements (provisional) for its
first 3 games — a clear rule rather than a hidden uncertainty threshold
(which left 9 of 14 teams in placements after round 2 for reasons
players couldn't see); the uncertainty only keeps a team in placements
after that if its rating is unusually unsettled.
Set chance, not expected score. Closeness is each team's chance of winning any one set (independent of how many sets fit in the hour). The admin Fixtures page shows it as "62% · 38%"; costs: 50/50 = 0, 67/33 = 1, 75/25 = 2.25, 80/20 = 3.2. Compared in simulation with ladder places apart, raw rating points and standard deviations apart — all similar in an evenly spread league; win chance edges ahead in a lumpy one (a few strong teams, a bunched middle).
Game-style public pages (RSL_Stats, teams only):
- Tiers (Ranked): Bronze < 1350 ≤ Silver < 1500 ≤ Gold < 1650 ≤
Platinum < 1800 ≤ Diamond. New teams are In placements until their
rating settles — no tier or rating change shown (which also keeps the
hidden starting estimate hidden). Names/bands in
RSL_Stats::TIERS. - Fixtures: tier and ladder place under each Ranked team; the night's rating change on results; an Upset! flag when the winner had under a 35% set chance; for coming games a match-up bar in the teams' colours with each side's set chance and a verdict (Even match / Close — slight edge / Upset alert).
- Ladder: tier and last-5 form for each team.
- Team page: a profile card under the name — tier (and peak), form, streak, journey (e.g. In placements → Silver R2 → Gold R4) and achievements: Giant killer, Comeback, Flawless, On fire (3 wins in a row), Double trouble (won both games of a double-header), Top spot. Each result shows the rating change and any upset win.
Automatic fixtures and the Fixtures admin (0.6.0)
Social League → Fixtures: one game night at a time — Make fixtures automatically, check them (each game shows each team's set chance, earlier meetings, double-headers and whether time preferences were met), adjust by hand (remove / add a game in a free slot), Publish, then enter results or forfeits. Published nights are locked; a night with results can't be remade.
How the engine (RSL_Pairing) pairs a night — every possible pairing
gets a cost in one unit, "a 2-to-1 matchup", and the cheapest full set
of games wins (exact matching up to 18 teams, a strong search above):
- Closeness (Ranked): from the two ratings, each team's chance of winning a set — 50/50 costs 0, 67/33 costs 1, 75/25 costs 2.25, 80/20 costs 3.2. New teams' ratings count as uncertain (pulled towards 50/50).
- Variety: a 2nd meeting costs 1 (the same as a 67/33 game); a 3rd costs 25, so twice a season is the cap unless there's no alternative. Last week's opponent never; two rounds ago +1.
- Unranked: no ratings, so purely least-played, longest-ago.
- Odd numbers: the spare team plays a double-header team, or sits out (dearer each time for the same team).
- Slots: the night fills from the first slot (no idle middle courts); "earlier"/"later" means first/last games of that night, missed preferences get priority next time, the last slot is shared around, and spare slots become double-headers for opted-in teams (back-to-back or with a break; never a third meeting or a lopsided game).
- Weights are in
RSL_Pairing::weights()(filterrsl_pairing_weights).
dev/simulate-pairing.php plays many pretend seasons through the real
code to compare strategies (14 teams, 10 rounds, 40 seasons each):
(Expected set gap = how one-sided games were by true strength, over a 3-set game; lower is closer.)
| Strategy | Expected set gap | 2–1 games | Opponents per team | 3rd+ meetings / season | Most meetings |
|---|---|---|---|---|---|
| Engine | 1.00 | 62% | 7.5 | 0 | 2 |
| Closeness only | 0.84 | 66% | 5.2 | 14.5 | 5 |
| Within 3 ladder places | 0.94 | 65% | 7.1 | 4.2 | 4 |
| Round robin | 1.36 | 54% | 10.0 | 0 | 1 |
Fixtures, results, ratings and ladders (0.5.0)
- Games fill the game-night slots from Courts & Nights: two teams of the division that court hosts that night. Checks: no games on a bye, real slots only, one game per court at a time, no team in two places at once (double-headers later in the night are fine). Removing a night from the season removes its games.
- Results are set scores, home first (
25-20, 23-25, 15-12). Timed games mean sets can end short, so the only rules are whole numbers, no tied sets, 1–5 sets. A forfeit records who gave the game away: a win/loss on the record, never a rating change. - Publishing is per night — nothing shows publicly until that week's fixtures are released.
- Ratings (Ranked) — Bradley-Terry on set results, refitted from the
whole season whenever read (
RSL_Ratings), on an Elo-style scale. Each team starts from a prior on its level estimate, recent sets weigh a little more, and each team has an uncertainty: provisional until it settles (about three games). A team with no games yet shows as New with no number, so its hidden starting estimate is never revealed. - Public pages (teams, never people):
- Fixtures & Results — week strip (rounds, bye, finals), each week by venue and time: court, teams, score and sets, "Result to come", or when fixtures come out. A signed-in player's games are highlighted.
- League Ladder — Ranked by rating (with record, sets and places moved since last week); Unranked by wins then set difference, no ratings.
- Team pages — standing (rating and place, or ladder place) and the team's fixtures and results.
- Fixtures and results are managed on the Fixtures admin page (0.6.0).
Team level and captain preferences (0.4.0)
- Approximate level — a captain applying for a team picks roughly
how the team plays: New, Social, Solid, Strong (VVL State League and
friends) or Advanced (VVL Premier League). It's an admin-only estimate,
never shown on public pages or to players, and admins can correct it on
the team page. Later it seeds each team's first Ranked rating
(
RSL_Teams::level_seed_rating(): 1200–1800, average 1500) so week-one games are even; real results take over from there. - Fixturing preferences — on the Social League page the captain sets, for the season: earlier / later / no preference, happy to double-header (and if so back-to-back or with a break), and standby for late games when another team pulls out. Admins see a one-line summary on the team page. Automatic fixturing will read these each week; they're wishes, not guarantees.
Courts and game nights (0.3.0)
Social League → Courts & Nights — what fixtures will be made from.
- Venues & courts — each venue has a home division (e.g. Ranked at one venue, Unranked at the other) and its courts, each with its usual game-night hours (a court can finish earlier than the rest).
- Season calendar — set the first and last game night, the weekday (Sundays), and the game length for Ranked and for Unranked. Saving lists every game night; re-saving keeps nights already set up and removes empty nights outside the new dates (a night already in use is kept and marked — see 0.12.0).
- The grid — one row per night, one column per venue: courts, hours, how many games and for which division, with byes greyed, finals marked and nights that differ from the usual flagged. Each night is Regular, Finals or Bye, with an optional note (e.g. a public holiday).
- One night — click it for a timeline: a bar per court across the evening, divided into game slots. Turn a court off, change its hours, or have a venue host the other division — for that night only — then optionally copy the setup to every later night. Setting a court back to its usual hours removes the exception.
The game slots (court, start, end, division) are what fixtures will fill and refs will claim.
Registration and team building (0.2.0)
A Social League page is created automatically ([rsl_my_league]).
- Register — free, a short profile: highest level played, years playing, positions, and an optional free agent flag (from 0.13.0, only with a complete club profile).
- Cleared to play — needed before a player's first game, not to join a team: a full or life club member (read live from the MURVC Club Manager) is covered; anyone else needs a social rego — 12
This README is longer than the copy stored here. Read the rest on GitHub →