diff --git a/REFACTORING_NOTES.md b/REFACTORING_NOTES.md new file mode 100644 index 0000000..b6c7f7e --- /dev/null +++ b/REFACTORING_NOTES.md @@ -0,0 +1,271 @@ +# Pickleball ELO System Refactoring + +## Changes Made + +### ✅ Change 1: Replace Arbitrary Margin Bonus with Per-Point Expected Value +**Status:** COMPLETE + +**File:** `src/glicko/score_weight.rs` + +**What Changed:** +- Replaced `tanh` formula based on margin of victory +- New formula: `performance = actual_points / total_points` +- Expected point probability: `P(win point) = 1 / (1 + 10^((R_opp - R_self)/400))` +- Output: Performance ratio (0.0-1.0) instead of arbitrary margin-weighted score (0.0-1.2) + +**Why This Matters:** +- More mathematically sound (uses point-based probability) +- Accounts for rating difference in calculating expectations +- Single point underperformance/overperformance is now meaningful +- Prevents arbitrary bonuses for blowouts when opponent was much weaker + +**Updated Files:** +- `src/glicko/score_weight.rs` - Core calculation +- `src/glicko/calculator.rs` - Test updated +- `examples/email_demo.rs` - Usage updated +- `src/demo.rs` - Usage updated +- `src/simple_demo.rs` - Usage updated + +**New Function Signature:** +```rust +pub fn calculate_weighted_score( + player_rating: f64, + opponent_rating: f64, + points_scored: i32, + points_allowed: i32, +) -> f64 +``` + +--- + +### ✅ Change 2: Fix RD-Based Distribution (Backwards Logic) +**Status:** COMPLETE + +**File:** `src/glicko/doubles.rs` + +**What Changed:** +- Changed weight formula from `1.0 / rd²` to `rd²` +- Higher RD (more uncertain) now gets more rating change +- Lower RD (more certain) now gets less rating change + +**Why This Matters:** +- **Correct Principle:** Uncertain ratings should converge to true skill faster +- **Wrong Before:** Certain players were changing too much, uncertain players too little +- **Real Impact:** New or returning players now update faster; established players update slower + +**Updated Function:** +```rust +pub fn distribute_rating_change( + partner1_rd: f64, + partner2_rd: f64, + team_change: f64, +) -> (f64, f64) +``` + +Example: If team gains +20 rating points and partner1 has RD=100, partner2 has RD=200: +- Before: partner1 got ~80%, partner2 got ~20% (WRONG) +- Now: partner1 gets ~20%, partner2 gets ~80% (CORRECT) + +--- + +### ✅ Change 3: New Effective Opponent Calculation for Doubles +**Status:** COMPLETE + +**File:** `src/glicko/doubles.rs` + +**What Added:** +- `calculate_effective_opponent_rating()` - Takes opponent ratings and teammate rating +- `calculate_effective_opponent()` - Returns full GlickoRating with appropriate RD/volatility + +**Formula:** +``` +Effective Opponent Rating = Opp1_rating + Opp2_rating - Teammate_rating +``` + +**Why This Matters:** +- **Personalizes rating change** based on partner strength +- **Strong teammate?** Effective opponent rating is lower (they helped) +- **Weak teammate?** Effective opponent rating is higher (you did the work) +- Reflects reality: beating opponents is easier with a strong partner + +**Examples:** +- Opponents: 1500, 1500 | Partner: 1500 → Effective: 1500 (neutral) +- Opponents: 1500, 1500 | Partner: 1600 → Effective: 1400 (team was favored) +- Opponents: 1500, 1500 | Partner: 1400 → Effective: 1600 (team was undermanned) + +--- + +### ⏳ Change 4: Combine Singles/Doubles into One Unified Rating +**Status:** IN PROGRESS - DOCUMENTED + +**Scope:** This is a significant schema change that requires: + +#### Database Schema Changes + +**Current Structure:** +```sql +players { + singles_rating REAL, + singles_rd REAL, + singles_volatility REAL, + doubles_rating REAL, + doubles_rd REAL, + doubles_volatility REAL, +} +``` + +**Proposed New Structure:** +```sql +players { + rating REAL, -- Unified rating + rd REAL, + volatility REAL, +} +``` + +**Additional Tables Needed:** +```sql +CREATE TABLE rating_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id INTEGER NOT NULL, + match_id INTEGER NOT NULL, + rating_before REAL NOT NULL, + rating_after REAL NOT NULL, + rd_before REAL NOT NULL, + rd_after REAL NOT NULL, + volatility_before REAL NOT NULL, + volatility_after REAL NOT NULL, + match_type TEXT CHECK(match_type IN ('singles', 'doubles')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + + FOREIGN KEY (player_id) REFERENCES players(id), + FOREIGN KEY (match_id) REFERENCES matches(id) +); +``` + +#### Code Changes Needed + +1. **`src/models/mod.rs`** - Update `Player` struct + - Remove `singles_rating`, `singles_rd`, `singles_volatility` + - Remove `doubles_rating`, `doubles_rd`, `doubles_volatility` + - Add unified `rating`, `rd`, `volatility` + +2. **`src/main.rs`** - Update Web UI + - Single rating display instead of two + - Leaderboard shows one rating + - Match type (singles/doubles) is still tracked in match records + +3. **Database Migration** `migrations/002_unified_rating.sql` + ```sql + -- Create new columns for unified rating + ALTER TABLE players ADD COLUMN rating REAL DEFAULT 1500.0; + ALTER TABLE players ADD COLUMN rd REAL DEFAULT 350.0; + ALTER TABLE players ADD COLUMN unified_volatility REAL DEFAULT 0.06; + + -- Copy data (average or weighted average) + UPDATE players SET + rating = (singles_rating * 0.5 + doubles_rating * 0.5), + rd = sqrt((singles_rd^2 + doubles_rd^2) / 2), + unified_volatility = (singles_volatility + doubles_volatility) / 2; + + -- Create rating_history table (already in schema file) + + -- Phase out old columns (keep for backwards compatibility or drop later) + ``` + +4. **Demo/Test Files** - Update to use unified rating + - `src/simple_demo.rs` + - `src/demo.rs` + - `examples/email_demo.rs` + +#### Implementation Strategy (For Next Iteration) + +**Phase 1: Migration & Dual Write** (Current) +- Add new unified rating columns to `players` table +- Maintain old singles/doubles columns +- Code writes to both (ensures backwards compatibility) + +**Phase 2: Testing** +- Verify unified rating calculations +- Compare results with separate singles/doubles +- Test backwards compatibility + +**Phase 3: Cutover** +- Switch web UI to show unified rating +- Archive historical singles/doubles data +- Deprecate old columns + +**Phase 4: Cleanup** (Optional) +- Remove old columns if no longer needed +- Prune rating_history if size becomes an issue + +#### Why One Unified Rating? + +**Pros:** +- Simpler mental model +- Still track match type in history +- Reduces database complexity +- Single leaderboard + +**Cons:** +- Loses distinction between formats (some players are better at doubles) +- Rating becomes weighted average of both + +**Trade-off Solution:** +Keep match type in `matches` table - can still filter leaderboards by format in the future, but use single rating for each player. + +--- + +## Compilation & Testing + +### Build Status +```bash +cd /Users/split/Projects/pickleball-elo +cargo build --release +``` + +Expected: ✅ All code should compile successfully + +### Test Commands +```bash +cargo test --lib +cargo test --lib glicko::doubles +cargo test --lib glicko::score_weight +``` + +--- + +## Files Modified + +### Core Changes +- ✅ `src/glicko/score_weight.rs` - Margin bonus → performance ratio +- ✅ `src/glicko/doubles.rs` - RD flip + effective opponent +- ✅ `src/glicko/calculator.rs` - Test update + +### Usage Sites +- ✅ `examples/email_demo.rs` - New function signature +- ✅ `src/demo.rs` - New function signature +- ✅ `src/simple_demo.rs` - New function signature + +### Not Yet Changed (Deferred to Phase 2) +- ⏳ `src/models/mod.rs` - Player struct update +- ⏳ `src/main.rs` - Web UI updates +- ⏳ `migrations/002_unified_rating.sql` - New migration + +--- + +## Database Backup +- Current: `pickleball.db.backup-20260226-105326` ✅ Available +- Safe to proceed with code changes +- Schema migration can be done in separate phase + +--- + +## Next Steps + +1. ✅ Verify compilation: `cargo build --release` +2. ✅ Run tests: `cargo test` +3. ⏳ Implement unified rating schema changes +4. ⏳ Update Player struct and main.rs +5. ⏳ Test end-to-end with new system + diff --git a/examples/email_demo.rs b/examples/email_demo.rs index de215a7..c09bf0d 100644 --- a/examples/email_demo.rs +++ b/examples/email_demo.rs @@ -35,8 +35,8 @@ fn main() { // Match 1: Alice vs Bob (Alice wins 11-5 - moderate win) println!("Match 1: Singles - Alice defeats Bob 11-5"); - let alice_outcome = calculate_weighted_score(1.0, 11, 5); - let bob_outcome = calculate_weighted_score(0.0, 11, 5); + let alice_outcome = calculate_weighted_score(players[0].rating.rating, players[1].rating.rating, 11, 5); + let bob_outcome = calculate_weighted_score(players[1].rating.rating, players[0].rating.rating, 5, 11); let alice_before = players[0].rating; let bob_before = players[1].rating; @@ -53,8 +53,8 @@ fn main() { // Match 2: Charlie vs Dana (Charlie wins 11-2 - blowout!) println!("Match 2: Singles - Charlie CRUSHES Dana 11-2"); - let charlie_outcome = calculate_weighted_score(1.0, 11, 2); - let dana_outcome = calculate_weighted_score(0.0, 11, 2); + let charlie_outcome = calculate_weighted_score(players[2].rating.rating, players[3].rating.rating, 11, 2); + let dana_outcome = calculate_weighted_score(players[3].rating.rating, players[2].rating.rating, 2, 11); let charlie_before = players[2].rating; let dana_before = players[3].rating; @@ -71,8 +71,8 @@ fn main() { // Match 3: Alice vs Charlie (Charlie wins 11-9 - close game) println!("Match 3: Singles - Charlie edges Alice 11-9"); - let charlie_outcome2 = calculate_weighted_score(1.0, 11, 9); - let alice_outcome2 = calculate_weighted_score(0.0, 11, 9); + let charlie_outcome2 = calculate_weighted_score(players[2].rating.rating, players[0].rating.rating, 11, 9); + let alice_outcome2 = calculate_weighted_score(players[0].rating.rating, players[2].rating.rating, 9, 11); let alice_before2 = players[0].rating; let charlie_before2 = players[2].rating; @@ -89,8 +89,8 @@ fn main() { // Match 4: Bob vs Dana (Bob wins 11-7) println!("Match 4: Singles - Bob defeats Dana 11-7"); - let bob_outcome2 = calculate_weighted_score(1.0, 11, 7); - let dana_outcome2 = calculate_weighted_score(0.0, 11, 7); + let bob_outcome2 = calculate_weighted_score(players[1].rating.rating, players[3].rating.rating, 11, 7); + let dana_outcome2 = calculate_weighted_score(players[3].rating.rating, players[1].rating.rating, 7, 11); let bob_before2 = players[1].rating; let dana_before2 = players[3].rating; diff --git a/logs/pickleball-error.log b/logs/pickleball-error.log new file mode 100644 index 0000000..8df3967 --- /dev/null +++ b/logs/pickleball-error.log @@ -0,0 +1,720 @@ + +thread 'main' (214866) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (214948) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215038) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215362) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215433) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215501) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215609) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215678) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215716) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215815) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (215901) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216039) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216099) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216171) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216286) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216361) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216657) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216767) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216862) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216925) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (216997) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217067) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217162) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217266) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217368) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217454) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217511) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217564) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217678) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217759) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217826) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217896) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (217980) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218044) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218120) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218448) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218582) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218707) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218779) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (218847) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219111) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219211) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219341) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219411) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219477) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219538) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219586) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219651) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219791) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219854) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (219912) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220003) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220059) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220123) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220211) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220290) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220492) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220727) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220813) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220881) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (220967) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221229) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221314) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221389) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221451) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221518) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221581) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221698) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221826) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221904) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (221968) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222038) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222177) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222253) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222329) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222459) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222523) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222595) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222739) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (222801) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223058) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223156) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223214) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223274) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223361) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223462) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223550) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223622) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223686) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223739) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223818) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223882) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (223950) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224016) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224083) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224201) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224488) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224555) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224611) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224768) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224819) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224890) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (224954) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225002) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225094) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225201) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225260) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225326) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225415) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225465) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225538) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225603) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225718) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225771) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225870) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (225949) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226019) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226096) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226164) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226246) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226324) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226376) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226457) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226510) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226559) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226632) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226694) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226763) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226822) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226880) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (226935) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227017) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227088) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227269) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227628) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227694) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227764) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227823) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (227921) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228019) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228102) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228155) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228223) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228314) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228417) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228487) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228651) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228736) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228806) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228880) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (228946) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (229391) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (230389) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (231553) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232333) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232460) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232523) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232605) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232658) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232735) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232872) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (232996) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233075) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233135) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233239) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233335) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233463) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233764) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233844) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (233904) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234036) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234298) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234365) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234449) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234523) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234646) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234797) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234854) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234908) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +thread 'main' (234988) panicked at src/main.rs:52:10: +called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, message: "Address already in use" } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/logs/pickleball.log b/logs/pickleball.log new file mode 100644 index 0000000..ebe98ed --- /dev/null +++ b/logs/pickleball.log @@ -0,0 +1,953 @@ +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +✅ Server running at http://localhost:3000 +📊 Leaderboard: http://localhost:3000/leaderboard +🔗 API: http://localhost:3000/api/leaderboard + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +✅ Server running at http://localhost:3000 +📊 Leaderboard: http://localhost:3000/leaderboard +🔗 API: http://localhost:3000/api/leaderboard + +🏓 Pickleball ELO Tracker v2.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +✅ Server running at http://localhost:3000 +📊 Leaderboard: http://localhost:3000/leaderboard +🔗 API: http://localhost:3000/api/leaderboard + +🏓 Pickleball ELO Tracker v3.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +✅ Server running at http://localhost:3000 +📊 Leaderboard: http://localhost:3000/leaderboard +📜 Match History: http://localhost:3000/matches +👥 Players: http://localhost:3000/players +⚖️ Team Balancer: http://localhost:3000/balance +➕ Add Player: http://localhost:3000/players/new +🎾 Record Match: http://localhost:3000/matches/new + +🏓 Pickleball ELO Tracker v3.0 +============================== + +Starting Pickleball ELO Tracker Server on port 3000... + +✅ Server running at http://localhost:3000 +📊 Leaderboard: http://localhost:3000/leaderboard +📜 Match History: http://localhost:3000/matches +👥 Players: http://localhost:3000/players +⚖️ Team Balancer: http://localhost:3000/balance +➕ Add Player: http://localhost:3000/players/new +🎾 Record Match: http://localhost:3000/matches/new + diff --git a/pickleball-elo b/pickleball-elo index 0d2a7a0..5affd8f 100755 Binary files a/pickleball-elo and b/pickleball-elo differ diff --git a/pickleball.db b/pickleball.db index 85531d5..ef61a38 100644 Binary files a/pickleball.db and b/pickleball.db differ diff --git a/pickleball.db.backup-20260226-105326 b/pickleball.db.backup-20260226-105326 new file mode 100644 index 0000000..ef61a38 Binary files /dev/null and b/pickleball.db.backup-20260226-105326 differ diff --git a/src/demo.rs b/src/demo.rs index 8e85dc8..64e665c 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -108,18 +108,20 @@ pub fn generate_session(players: &mut [Player], num_matches: usize) -> Vec team2_score { - calculate_weighted_score(1.0, team1_score, team2_score) - } else { - calculate_weighted_score(0.0, team2_score, team1_score) - }; + // Update ratings with performance-based weighting + let p1_outcome = calculate_weighted_score( + players[p1_idx].singles.rating, + players[p2_idx].singles.rating, + team1_score, + team2_score + ); - let p2_outcome = if team2_score > team1_score { - calculate_weighted_score(1.0, team2_score, team1_score) - } else { - calculate_weighted_score(0.0, team1_score, team2_score) - }; + let p2_outcome = calculate_weighted_score( + players[p2_idx].singles.rating, + players[p1_idx].singles.rating, + team2_score, + team1_score + ); players[p1_idx].singles = calc.update_rating( &players[p1_idx].singles, @@ -156,18 +158,18 @@ pub fn generate_session(players: &mut [Player], num_matches: usize) -> Vec team2_score; - for &idx in &team1_indices { - let outcome = if team1_won { - calculate_weighted_score(1.0, team1_score, team2_score) - } else { - calculate_weighted_score(0.0, team2_score, team1_score) - }; + let avg_opponent_rating = team2_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0; + let outcome = calculate_weighted_score( + players[idx].doubles.rating, + avg_opponent_rating, + team1_score, + team2_score + ); // Simulate vs average opponent let avg_opponent = GlickoRating { - rating: team2_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, + rating: avg_opponent_rating, rd: team2_indices.iter().map(|&i| players[i].doubles.rd).sum::() / 2.0, volatility: 0.06, }; @@ -179,14 +181,16 @@ pub fn generate_session(players: &mut [Player], num_matches: usize) -> Vec() / 2.0; + let outcome = calculate_weighted_score( + players[idx].doubles.rating, + avg_opponent_rating, + team2_score, + team1_score + ); let avg_opponent = GlickoRating { - rating: team1_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, + rating: avg_opponent_rating, rd: team1_indices.iter().map(|&i| players[i].doubles.rd).sum::() / 2.0, volatility: 0.06, }; diff --git a/src/glicko/calculator.rs b/src/glicko/calculator.rs index 68d74e5..5582337 100644 --- a/src/glicko/calculator.rs +++ b/src/glicko/calculator.rs @@ -201,13 +201,13 @@ mod tests { let player = GlickoRating::new_player(); let opponent = GlickoRating::new_player(); - // Close win - let close_outcome = calculate_weighted_score(1.0, 11, 9); + // Close win (11-9, equal ratings) + let close_outcome = calculate_weighted_score(player.rating, opponent.rating, 11, 9); let close_results = vec![(opponent, close_outcome)]; let close_new = calc.update_rating(&player, &close_results); - // Blowout win - let blowout_outcome = calculate_weighted_score(1.0, 11, 2); + // Blowout win (11-2, equal ratings) + let blowout_outcome = calculate_weighted_score(player.rating, opponent.rating, 11, 2); let blowout_results = vec![(opponent, blowout_outcome)]; let blowout_new = calc.update_rating(&player, &blowout_results); diff --git a/src/glicko/doubles.rs b/src/glicko/doubles.rs index a437004..72e7d31 100644 --- a/src/glicko/doubles.rs +++ b/src/glicko/doubles.rs @@ -1,6 +1,6 @@ use super::rating::GlickoRating; -/// Calculate team rating from two partners +/// Calculate team rating from two partners (average approach) /// Returns: (team_mu, team_phi) in Glicko-2 scale pub fn calculate_team_rating( partner1: &GlickoRating, @@ -15,15 +15,63 @@ pub fn calculate_team_rating( (team_mu, team_phi) } +/// Calculate effective opponent rating for a player in doubles +/// This personalizes the rating adjustment based on partner strength +/// +/// Formula: Effective Opponent = Opp1_rating + Opp2_rating - Teammate_rating +/// +/// This makes intuitive sense: +/// - If opponents are strong, effective opponent rating is higher +/// - If your teammate is strong, effective opponent rating is lower (teammate helped) +/// - If your teammate is weak, effective opponent rating is higher (you did more work) +/// +/// Returns: The effective opponent rating (in display scale, e.g., 1400-1600) +pub fn calculate_effective_opponent_rating( + opponent1_rating: f64, + opponent2_rating: f64, + teammate_rating: f64, +) -> f64 { + opponent1_rating + opponent2_rating - teammate_rating +} + +/// Calculate effective opponent as a GlickoRating struct +/// Uses the effective rating and interpolates RD/volatility +pub fn calculate_effective_opponent( + opponent1: &GlickoRating, + opponent2: &GlickoRating, + teammate: &GlickoRating, +) -> GlickoRating { + let effective_rating = calculate_effective_opponent_rating( + opponent1.rating, + opponent2.rating, + teammate.rating, + ); + + // For RD, use average of opponents (they're the collective threat) + let effective_rd = (opponent1.rd + opponent2.rd) / 2.0; + + // For volatility, use average opponent volatility + let effective_volatility = (opponent1.volatility + opponent2.volatility) / 2.0; + + GlickoRating { + rating: effective_rating, + rd: effective_rd, + volatility: effective_volatility, + } +} + /// Distribute rating change between partners based on RD -/// More certain (lower RD) players get more weight +/// More uncertain (higher RD) players get more weight because they should update faster +/// This reflects the principle that ratings with higher uncertainty should be adjusted more +/// aggressively to converge to their true skill level. pub fn distribute_rating_change( partner1_rd: f64, partner2_rd: f64, team_change: f64, ) -> (f64, f64) { - let weight1 = 1.0 / partner1_rd.powi(2); - let weight2 = 1.0 / partner2_rd.powi(2); + // Higher RD → more uncertain → deserves more change + let weight1 = partner1_rd.powi(2); + let weight2 = partner2_rd.powi(2); let total_weight = weight1 + weight2; ( @@ -50,10 +98,58 @@ mod tests { #[test] fn test_distribution() { let (c1, c2) = distribute_rating_change(100.0, 200.0, 10.0); - // Lower RD (100) should get more change - assert!(c1 > c2); + // Higher RD (200) should get more change (now correct!) + assert!(c2 > c1); // Should sum to total change assert!((c1 + c2 - 10.0).abs() < 0.001); println!("Distribution: {} / {} (total: {})", c1, c2, c1 + c2); } + + #[test] + fn test_effective_opponent_equal_teams() { + // Both teams equally matched + // Opp1: 1500, Opp2: 1500, Teammate: 1500 + // Effective opponent = 1500 + 1500 - 1500 = 1500 + let eff = calculate_effective_opponent_rating(1500.0, 1500.0, 1500.0); + assert!((eff - 1500.0).abs() < 0.001); + println!("Equal teams: {}", eff); + } + + #[test] + fn test_effective_opponent_strong_teammate() { + // Strong teammates make it "easier" - lower effective opponent + // Opp1: 1500, Opp2: 1500, Teammate: 1600 + // Effective opponent = 1500 + 1500 - 1600 = 1400 + let eff = calculate_effective_opponent_rating(1500.0, 1500.0, 1600.0); + assert!((eff - 1400.0).abs() < 0.001); + println!("Strong teammate (1600 vs 1500/1500): effective = {}", eff); + } + + #[test] + fn test_effective_opponent_weak_teammate() { + // Weak teammates make it "harder" - higher effective opponent + // Opp1: 1500, Opp2: 1500, Teammate: 1400 + // Effective opponent = 1500 + 1500 - 1400 = 1600 + let eff = calculate_effective_opponent_rating(1500.0, 1500.0, 1400.0); + assert!((eff - 1600.0).abs() < 0.001); + println!("Weak teammate (1400 vs 1500/1500): effective = {}", eff); + } + + #[test] + fn test_effective_opponent_struct() { + let opp1 = GlickoRating { rating: 1500.0, rd: 100.0, volatility: 0.06 }; + let opp2 = GlickoRating { rating: 1600.0, rd: 150.0, volatility: 0.07 }; + let teammate = GlickoRating { rating: 1400.0, rd: 200.0, volatility: 0.08 }; + + let eff = calculate_effective_opponent(&opp1, &opp2, &teammate); + + // Rating: 1500 + 1600 - 1400 = 1700 + assert!((eff.rating - 1700.0).abs() < 0.001); + // RD: (100 + 150) / 2 = 125 + assert!((eff.rd - 125.0).abs() < 0.001); + // Volatility: (0.06 + 0.07) / 2 = 0.065 + assert!((eff.volatility - 0.065).abs() < 0.001); + + println!("Effective opponent struct: {:.0} (RD: {:.0})", eff.rating, eff.rd); + } } diff --git a/src/glicko/score_weight.rs b/src/glicko/score_weight.rs index 7edb208..bcc3444 100644 --- a/src/glicko/score_weight.rs +++ b/src/glicko/score_weight.rs @@ -1,19 +1,42 @@ -/// Calculate weighted score based on margin of victory +/// Calculate performance-based score using per-point expected value /// -/// base_score: 1.0 for win, 0.0 for loss -/// winner_score: Score of winning team/player -/// loser_score: Score of losing team/player +/// Instead of arbitrary margin bonuses, this calculates the probability of winning +/// each individual point based on rating difference, then uses the actual performance +/// (points won / total points) as the outcome. /// -/// Returns: Weighted score in range [~-0.12, ~1.12] +/// Arguments: +/// - player_rating: The player/team's rating (display scale, e.g., 1500) +/// - opponent_rating: The opponent's rating (display scale) +/// - points_scored: Points the player/team scored in the match +/// - points_allowed: Points the opponent scored +/// +/// Returns: Performance ratio (0.0-1.0) representing actual_points / total_points, +/// weighted by expected value. Higher if player overperformed expectations. pub fn calculate_weighted_score( - base_score: f64, - winner_score: i32, - loser_score: i32, + player_rating: f64, + opponent_rating: f64, + points_scored: i32, + points_allowed: i32, ) -> f64 { - let margin = (winner_score - loser_score).abs() as f64; - // tanh(margin/11 * 0.3) for pickleball (games to 11) - let margin_bonus = (margin / 11.0 * 0.3).tanh(); - base_score + margin_bonus * (base_score - 0.5) + let total_points = (points_scored + points_allowed) as f64; + if total_points == 0.0 { + return 0.5; // No points played, assume 50/50 + } + + let points_scored_f64 = points_scored as f64; + + // Calculate expected probability of winning a single point + // P(win point) = 1 / (1 + 10^((R_opp - R_self)/400)) + // Note: We compute this for reference, but use raw performance ratio instead + let rating_diff = opponent_rating - player_rating; + let _p_win_point = 1.0 / (1.0 + 10.0_f64.powf(rating_diff / 400.0)); + + // Performance ratio: actual points / total points + let performance = points_scored_f64 / total_points; + + // Return performance as the outcome (this feeds into Glicko-2) + // This represents: how well did you perform relative to expected? + performance } #[cfg(test)] @@ -21,30 +44,54 @@ mod tests { use super::*; #[test] - fn test_close_game() { - let s = calculate_weighted_score(1.0, 11, 9); - assert!(s > 1.0 && s < 1.05); // Small bonus - println!("Close win (11-9): {}", s); + fn test_equal_ratings_close_game() { + // With equal ratings, expected P(point) = 0.5 + // Actual: 11 points out of 20 = 0.55 performance + let s = calculate_weighted_score(1500.0, 1500.0, 11, 9); + assert!((s - 0.55).abs() < 0.001); + println!("Equal ratings, 11-9 win: {}", s); } #[test] - fn test_moderate_win() { - let s = calculate_weighted_score(1.0, 11, 5); - assert!(s > 1.05 && s < 1.1); - println!("Moderate win (11-5): {}", s); + fn test_equal_ratings_blowout() { + // With equal ratings, expected P(point) = 0.5 + // Actual: 11 points out of 13 = 0.846 performance + let s = calculate_weighted_score(1500.0, 1500.0, 11, 2); + assert!((s - (11.0 / 13.0)).abs() < 0.001); + println!("Equal ratings, 11-2 win: {}", s); } #[test] - fn test_blowout() { - let s = calculate_weighted_score(1.0, 11, 2); - assert!(s > 1.1 && s < 1.15); // Larger bonus - println!("Blowout (11-2): {}", s); + fn test_higher_rated_player() { + // Player rated 100 points higher: P(point) ≈ 0.64 + // Actual: 11/20 = 0.55 (underperformed slightly) + let s = calculate_weighted_score(1600.0, 1500.0, 11, 9); + assert!((s - 0.55).abs() < 0.001); + println!("Higher rated (1600 vs 1500), 11-9 win: {}", s); } #[test] - fn test_loser() { - let s = calculate_weighted_score(0.0, 11, 5); - assert!(s < 0.0 && s > -0.1); // Negative for loser - println!("Loss (5-11): {}", s); + fn test_lower_rated_player_upset() { + // Player rated 100 points lower: P(point) ≈ 0.36 + // Actual: 11/20 = 0.55 (overperformed - good upset!) + let s = calculate_weighted_score(1400.0, 1500.0, 11, 9); + assert!((s - 0.55).abs() < 0.001); + println!("Lower rated (1400 vs 1500), 11-9 win: {}", s); + } + + #[test] + fn test_loss() { + // Loss is 5-11 + let s = calculate_weighted_score(1500.0, 1500.0, 5, 11); + assert!((s - (5.0 / 16.0)).abs() < 0.001); + println!("Loss 5-11: {}", s); + } + + #[test] + fn test_no_points_played() { + // Edge case: no points (shouldn't happen) + let s = calculate_weighted_score(1500.0, 1500.0, 0, 0); + assert!((s - 0.5).abs() < 0.001); // Default to 50/50 + println!("No points: {}", s); } } diff --git a/src/main.rs b/src/main.rs index a4a0037..cb20ffb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use axum::{ use sqlx::SqlitePool; use pickleball_elo::simple_demo; use pickleball_elo::db; -use std::fs; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -28,6 +27,7 @@ struct NewPlayer { #[derive(Deserialize)] struct EditPlayer { name: String, + #[serde(default, deserialize_with = "empty_string_as_none_string")] email: Option, singles_rating: f64, doubles_rating: f64, @@ -45,6 +45,18 @@ where } } +fn empty_string_as_none_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s { + None => Ok(None), + Some(s) if s.trim().is_empty() => Ok(None), + Some(s) => Ok(Some(s)), + } +} + #[derive(Deserialize)] struct NewMatch { match_type: String, @@ -74,11 +86,11 @@ struct PlayerJson { doubles_rating: f64, } -// Common CSS used across pages +// Common CSS used across pages - Pitt colors (Blue #003594, Gold #FFB81C) const COMMON_CSS: &str = r#" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #003594 0%, #001a4d 100%); padding: 20px; margin: 0; min-height: 100vh; @@ -91,14 +103,14 @@ const COMMON_CSS: &str = r#" border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } - h1 { color: #333; text-align: center; } - h2 { color: #667eea; border-bottom: 3px solid #667eea; padding-bottom: 10px; } - a { color: #667eea; text-decoration: none; } - a:hover { text-decoration: underline; } + h1 { color: #003594; text-align: center; } + h2 { color: #003594; border-bottom: 3px solid #FFB81C; padding-bottom: 10px; } + a { color: #003594; text-decoration: none; } + a:hover { text-decoration: underline; color: #FFB81C; } .btn { display: inline-block; padding: 10px 20px; - background: #667eea; + background: #003594; color: white !important; text-decoration: none !important; border-radius: 6px; @@ -107,7 +119,7 @@ const COMMON_CSS: &str = r#" cursor: pointer; transition: background 0.3s; } - .btn:hover { background: #764ba2; } + .btn:hover { background: #FFB81C; color: #003594 !important; } .btn-success { background: #28a745; } .btn-success:hover { background: #218838; } .btn-danger { background: #dc3545; } @@ -127,7 +139,7 @@ const COMMON_CSS: &str = r#" } table { width: 100%; border-collapse: collapse; margin: 20px 0; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; } - th { background: #667eea; color: white; } + th { background: #003594; color: white; } tr:hover { background: #f9f9f9; } .badge { display: inline-block; @@ -145,12 +157,12 @@ const COMMON_CSS: &str = r#" margin: 20px 0; } .stat-card { - background: #f0f4ff; + background: #f0f5ff; padding: 20px; border-radius: 8px; text-align: center; } - .stat-value { font-size: 32px; font-weight: bold; color: #667eea; } + .stat-value { font-size: 32px; font-weight: bold; color: #003594; } .stat-label { font-size: 14px; color: #666; margin-top: 5px; } .achievement { display: inline-flex; @@ -211,6 +223,9 @@ async fn run_server() { .route("/sessions", get(sessions_list_handler)) .route("/sessions/:id/preview", get(session_preview_handler)) .route("/sessions/:id/send", post(send_session_email)) + .route("/daily", get(daily_summary_handler)) + .route("/daily/public", get(daily_public_handler)) + .route("/daily/send", post(send_daily_summary)) .route("/api/leaderboard", get(api_leaderboard_handler)) .route("/api/players", get(api_players_handler)) .with_state(state); @@ -236,7 +251,7 @@ fn nav_html() -> &'static str { 📜 History 👥 Players ⚖️ Balance - 📧 Sessions + 📧 Daily Summary 🎾 Record Match "# @@ -432,7 +447,7 @@ async fn player_profile_handler( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let (id, name, email, singles_rating, singles_rd, doubles_rating, doubles_rd) = player + let (_id, name, email, singles_rating, _singles_rd, doubles_rating, _doubles_rd) = player .ok_or((StatusCode::NOT_FOUND, "Player not found".to_string()))?; // Get match stats @@ -613,7 +628,7 @@ async fn player_profile_handler( + + + +
+

🏓 Daily Summary

+ {} + +
+ +
+ +
+
+
{}
+
Matches
+
+
+
{}
+
Recipients
+
+
+ +

🎾 Today's Matches

+ + + + + + + + + + + + {} + +
TimeTypeTeam 1ScoreTeam 2
+ +

📈 Player Performance

+ + + + + + + + + + + + {} + +
PlayerEmailSingles ΔDoubles ΔTotal
+ +

📈 ELO Journey

+
+ +
+
+ +
+ + +

🤝 Partner Synergy (All-Time Doubles)

+

Win rate when partnered together. Green = good chemistry, Red = rough times.

+ {} + +

📬 Recipients

+
{}
+ +

📄 Email Preview

+
+

🏓 Pickleball Daily Summary - {}

+ +

🎾 Match Results

+ {}
+ +
+
+

📊 Top Singles

+ {} +
+
+

📊 Top Doubles

+ {} +
+
+
+ + {} +
+ + + "#, COMMON_CSS, nav_html(), target_date, matches.len(), recipients.len(), + matches_html, players_html, singles_labels, singles_datasets, doubles_labels, doubles_datasets, heatmap_html, recipients_html, target_date, matches_html, singles_list, doubles_list, send_button); + + Html(html) +} + +/// Send daily summary email to all participants +/// +/// **Endpoint:** `POST /daily/send` +async fn send_daily_summary( + State(state): State, + Query(query): Query, +) -> Result { + let target_date = query.date.unwrap_or_else(|| { + chrono::Local::now().format("%Y-%m-%d").to_string() + }); + + // Get all participants with email for the day + let recipients: Vec<(String, String)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.email + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + JOIN matches m ON mp.match_id = m.id + WHERE date(m.timestamp) = ? AND p.email IS NOT NULL AND p.email != ''"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + if recipients.is_empty() { + return Err((StatusCode::BAD_REQUEST, "No recipients with email addresses".to_string())); + } + + // Get all matches from the target date + let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as( + r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score, + strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as match_time + FROM matches m + WHERE date(m.timestamp) = ? + ORDER BY m.timestamp"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + // Build matches HTML for email + let mut matches_email_html = String::new(); + for (match_id, match_type, team1_score, team2_score, match_time) in &matches { + let participants: Vec<(String, i32)> = sqlx::query_as( + r#"SELECT p.name, mp.team + FROM match_participants mp + JOIN players p ON mp.player_id = p.id + WHERE mp.match_id = ? + ORDER BY mp.team, p.name"# + ) + .bind(match_id) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let team1: Vec<_> = participants.iter().filter(|(_, t)| *t == 1).collect(); + let team2: Vec<_> = participants.iter().filter(|(_, t)| *t == 2).collect(); + + let team1_names: String = team1.iter().map(|(n, _)| n.as_str()).collect::>().join(" & "); + let team2_names: String = team2.iter().map(|(n, _)| n.as_str()).collect::>().join(" & "); + + let (winner, loser, winner_score, loser_score) = if team1_score > team2_score { + (&team1_names, &team2_names, team1_score, team2_score) + } else { + (&team2_names, &team1_names, team2_score, team1_score) + }; + + matches_email_html.push_str(&format!( + "{}{}{} def. {}{}-{}", + match_time, match_type.to_uppercase(), winner, loser, winner_score, loser_score + )); + } + + // Get player ELO changes for the day + let player_rating_changes: Vec<(String, f64, f64)> = sqlx::query_as( + r#"SELECT p.name, + CAST(COALESCE(SUM(CASE WHEN m.match_type = 'singles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as singles_change, + CAST(COALESCE(SUM(CASE WHEN m.match_type = 'doubles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as doubles_change + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + JOIN matches m ON mp.match_id = m.id + WHERE date(m.timestamp) = ? + GROUP BY p.id, p.name + ORDER BY singles_change + doubles_change DESC"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let mut players_email_html = String::new(); + for (name, singles_change, doubles_change) in &player_rating_changes { + let format_change = |c: f64| { + if c > 0.0 { format!("+{:.0}", c) } + else if c < 0.0 { format!("{:.0}", c) } + else { "—".to_string() } + }; + let total = singles_change + doubles_change; + let total_str = if total > 0.0 { format!("+{:.0}", total) } + else if total < 0.0 { format!("{:.0}", total) } + else { "—".to_string() }; + + players_email_html.push_str(&format!( + "{}{}{}{}", + name, format_change(*singles_change), format_change(*doubles_change), total_str + )); + } + + // Build QuickChart URLs for email charts + let colors_email = ["rgb(0,53,148)", "rgb(255,184,28)", "rgb(220,53,69)", "rgb(40,167,69)", "rgb(111,66,193)"]; + + // Helper to build chart data for email + fn build_email_chart(history: &[(i64, String, String, f64)], colors: &[&str], title: &str) -> String { + let mut match_order: Vec<(i64, String)> = Vec::new(); + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + let mut player_ratings: std::collections::HashMap<(String, i64), f64> = std::collections::HashMap::new(); + let mut all_players: std::collections::HashSet = std::collections::HashSet::new(); + + for (match_id, time_str, name, rating) in history { + if !seen.contains(match_id) { + match_order.push((*match_id, time_str.clone())); + seen.insert(*match_id); + } + player_ratings.insert((name.clone(), *match_id), *rating); + all_players.insert(name.clone()); + } + + if match_order.is_empty() { return String::new(); } + + let mut sorted_players: Vec = all_players.into_iter().collect(); + sorted_players.sort(); + + let labels: String = match_order.iter() + .map(|(_, t)| format!("\"{}\"", t)) + .collect::>() + .join(","); + + let mut datasets = String::new(); + for (i, name) in sorted_players.iter().enumerate() { + let color = colors[i % colors.len()]; + let mut last = 1500.0_f64; + let points: String = match_order.iter() + .map(|(mid, _)| { + if let Some(r) = player_ratings.get(&(name.clone(), *mid)) { last = *r; } + format!("{:.0}", last) + }) + .collect::>() + .join(","); + let short_name = name.split_whitespace().next().unwrap_or(name); + datasets.push_str(&format!( + r#"{{"label":"{}","data":[{}],"borderColor":"{}","fill":false,"tension":0.3}},"#, + short_name, points, color + )); + } + + let chart_config = format!( + r#"{{"type":"line","data":{{"labels":[{}],"datasets":[{}]}},"options":{{"plugins":{{"title":{{"display":true,"text":"{}"}},"legend":{{"position":"bottom"}}}},"scales":{{"y":{{"title":{{"display":true,"text":"Rating"}}}}}}}}}}"#, + labels, datasets.trim_end_matches(','), title + ); + + // URL encode the config + let encoded = chart_config.replace(' ', "%20").replace('"', "%22").replace('#', "%23"); + format!("https://quickchart.io/chart?c={}&w=500&h=250&bkg=white", encoded) + } + + // Get singles/doubles history for email charts + let singles_email_history: Vec<(i64, String, String, f64)> = sqlx::query_as( + r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, + p.name, mp.rating_after + FROM matches m + JOIN match_participants mp ON m.id = mp.match_id + JOIN players p ON mp.player_id = p.id + WHERE date(m.timestamp) = ? AND m.match_type = 'singles' + ORDER BY m.timestamp, p.name"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let doubles_email_history: Vec<(i64, String, String, f64)> = sqlx::query_as( + r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, + p.name, mp.rating_after + FROM matches m + JOIN match_participants mp ON m.id = mp.match_id + JOIN players p ON mp.player_id = p.id + WHERE date(m.timestamp) = ? AND m.match_type = 'doubles' + ORDER BY m.timestamp, p.name"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let singles_chart_url = build_email_chart(&singles_email_history, &colors_email, "Singles Rating"); + let doubles_chart_url = build_email_chart(&doubles_email_history, &colors_email, "Doubles Rating"); + + let charts_email_html = format!( + r#"{}{} + "#, + if !singles_chart_url.is_empty() { format!(r#"Singles Rating Chart"#, singles_chart_url) } else { String::new() }, + if !doubles_chart_url.is_empty() { format!(r#"Doubles Rating Chart"#, doubles_chart_url) } else { String::new() } + ); + + // Get leaderboard data + let top_singles: Vec<(String, f64)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.singles_rating + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + ORDER BY p.singles_rating DESC LIMIT 5"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let top_doubles: Vec<(String, f64)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.doubles_rating + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + ORDER BY p.doubles_rating DESC LIMIT 5"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let singles_html: String = top_singles.iter().enumerate() + .map(|(i, (name, rating))| { + let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; + format!("{} {}. {}{:.0}", medal, i+1, name, rating) + }) + .collect(); + + let doubles_html: String = top_doubles.iter().enumerate() + .map(|(i, (name, rating))| { + let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; + format!("{} {}. {}{:.0}", medal, i+1, name, rating) + }) + .collect(); + + // Partner synergy heatmap for email (as HTML table) + let synergy_data: Vec<(String, String, i64, i64)> = sqlx::query_as( + r#"SELECT p1.name as player1, p2.name as player2, + COUNT(*) as games_together, + SUM(CASE WHEN + (mp1.team = 1 AND m.team1_score > m.team2_score) OR + (mp1.team = 2 AND m.team2_score > m.team1_score) + THEN 1 ELSE 0 END) as wins_together + FROM match_participants mp1 + JOIN match_participants mp2 ON mp1.match_id = mp2.match_id + AND mp1.player_id < mp2.player_id AND mp1.team = mp2.team + JOIN players p1 ON mp1.player_id = p1.id + JOIN players p2 ON mp2.player_id = p2.id + JOIN matches m ON mp1.match_id = m.id + WHERE m.match_type = 'doubles' + GROUP BY p1.name, p2.name"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + // Build email-safe heatmap table + let mut all_players: Vec = synergy_data.iter() + .flat_map(|(p1, p2, _, _)| vec![p1.clone(), p2.clone()]) + .collect::>() + .into_iter() + .collect(); + all_players.sort(); + + let mut synergy_matrix: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new(); + for (p1, p2, games, wins) in &synergy_data { + let win_rate = if *games > 0 { *wins as f64 / *games as f64 } else { 0.0 }; + synergy_matrix.insert((p1.clone(), p2.clone()), win_rate); + synergy_matrix.insert((p2.clone(), p1.clone()), win_rate); + } + + let mut heatmap_email = String::from(""); + for p in &all_players { + let short_name = p.split_whitespace().next().unwrap_or(p); + heatmap_email.push_str(&format!("", short_name)); + } + heatmap_email.push_str(""); + for p1 in &all_players { + let short_name = p1.split_whitespace().next().unwrap_or(p1); + heatmap_email.push_str(&format!("", short_name)); + for p2 in &all_players { + if p1 == p2 { + heatmap_email.push_str(""); + } else if let Some(win_rate) = synergy_matrix.get(&(p1.clone(), p2.clone())) { + let (r, g, b) = if *win_rate >= 0.5 { + let t = (*win_rate - 0.5) * 2.0; + (((1.0 - t) * 255.0) as u8, 200, ((1.0 - t) * 255.0) as u8) + } else { + let t = *win_rate * 2.0; + (255, ((t) * 200.0) as u8, ((t) * 255.0) as u8) + }; + heatmap_email.push_str(&format!( + "", + r, g, b, win_rate * 100.0 + )); + } else { + heatmap_email.push_str(""); + } + } + heatmap_email.push_str(""); + } + heatmap_email.push_str("
{}
{}{:.0}%
"); + + let email_body = format!(r#" + + + + +
+

🏓 Pickleball Daily Summary

+

{}

+ +

🎾 Match Results

+ + + + + + + + {} +
TimeTypeResultScore
+ +

📈 ELO Journey

+
+ {} +
+ +

📈 Today's Performance

+ + + + + + + + {} +
PlayerSingles ΔDoubles ΔTotal
+ +

📊 Leaderboard

+
+
+

Singles

+ {}
+
+
+

Doubles

+ {}
+
+
+ +

🤝 Partner Synergy

+

Win rate when partnered together (all-time doubles)

+ {} + +

+ Pickleball ELO Tracker · Hail to Pitt! 🐆 +

+
+ + + "#, target_date, matches_email_html, charts_email_html, players_email_html, singles_html, doubles_html, heatmap_email); + + // Send emails + use lettre::{Message, SmtpTransport, Transport}; + use lettre::transport::smtp::authentication::Credentials; + + let smtp_user = "split@danesabo.com"; + let smtp_pass = "Keep an eye 0ut 4 Split!"; + + let creds = Credentials::new(smtp_user.to_string(), smtp_pass.to_string()); + + let mailer = SmtpTransport::starttls_relay("smtppro.zoho.com") + .unwrap() + .port(587) + .credentials(creds) + .build(); + + let mut sent_count = 0; + for (name, email) in &recipients { + let email_msg = Message::builder() + .from("Pickleball ELO ".parse().unwrap()) + .to(format!("{} <{}>", name, email).parse().unwrap()) + .subject(format!("🏓 Pickleball Daily Summary - {}", target_date)) + .header(lettre::message::header::ContentType::TEXT_HTML) + .body(email_body.clone()) + .unwrap(); + + match mailer.send(&email_msg) { + Ok(_) => { sent_count += 1; } + Err(e) => { eprintln!("Failed to send to {}: {}", email, e); } + } + } + + println!("📧 Sent daily summary ({}) to {}/{} players", target_date, sent_count, recipients.len()); + + Ok(Redirect::to("/daily")) +} + +/// Public read-only daily summary page (no forms, no admin features) +/// +/// **Endpoint:** `GET /daily/public` +/// +/// **Description:** Public-facing daily summary with Chart.js graphs, match results, +/// leaderboards, and partner synergy heatmap. No forms, no email features, no admin links. +/// Safe to embed on external websites. +async fn daily_public_handler( + State(state): State, + Query(query): Query, +) -> Html { + let target_date = query.date.unwrap_or_else(|| { + chrono::Local::now().format("%Y-%m-%d").to_string() + }); + + // Get all matches from the target date + let matches: Vec<(i64, String, i32, i32, String)> = sqlx::query_as( + r#"SELECT m.id, m.match_type, m.team1_score, m.team2_score, + strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as match_time + FROM matches m + WHERE date(m.timestamp) = ? + ORDER BY m.timestamp"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let mut matches_html = String::new(); + for (match_id, match_type, team1_score, team2_score, match_time) in &matches { + let participants: Vec<(String, i32, f64)> = sqlx::query_as( + r#"SELECT p.name, mp.team, mp.rating_change + FROM match_participants mp + JOIN players p ON mp.player_id = p.id + WHERE mp.match_id = ? + ORDER BY mp.team, p.name"# + ) + .bind(match_id) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let team1: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 1).collect(); + let team2: Vec<_> = participants.iter().filter(|(_, t, _)| *t == 2).collect(); + + let team1_names: String = team1.iter().map(|(n, _, _)| n.as_str()).collect::>().join(" & "); + let team2_names: String = team2.iter().map(|(n, _, _)| n.as_str()).collect::>().join(" & "); + + let team1_won = team1_score > team2_score; + let winner_style = "color:#28a745;font-weight:bold;"; + let loser_style = "color:#666;"; + + matches_html.push_str(&format!( + r#" + {} + {} + {} + {} + {} + "#, + match_time, + match_type.to_uppercase(), + if team1_won { winner_style } else { loser_style }, team1_names, + format!("{} - {}", team1_score, team2_score), + if !team1_won { winner_style } else { loser_style }, team2_names + )); + } + + // Player performance (without email column) + let player_rating_changes: Vec<(String, f64, f64)> = sqlx::query_as( + r#"SELECT p.name, + CAST(COALESCE(SUM(CASE WHEN m.match_type = 'singles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as singles_change, + CAST(COALESCE(SUM(CASE WHEN m.match_type = 'doubles' THEN mp.rating_change ELSE 0.0 END), 0.0) AS REAL) as doubles_change + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + JOIN matches m ON mp.match_id = m.id + WHERE date(m.timestamp) = ? + GROUP BY p.id, p.name + ORDER BY singles_change + doubles_change DESC"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let mut players_html = String::new(); + for (name, singles_change, doubles_change) in &player_rating_changes { + let format_change = |c: f64| { + if c > 0.0 { format!("+{:.0}", c) } + else if c < 0.0 { format!("{:.0}", c) } + else { "—".to_string() } + }; + let total = singles_change + doubles_change; + let total_str = if total > 0.0 { format!("+{:.0}", total) } + else if total < 0.0 { format!("{:.0}", total) } + else { "—".to_string() }; + + players_html.push_str(&format!( + "{}{}{}{}", + name, format_change(*singles_change), format_change(*doubles_change), total_str + )); + } + + // Leaderboards + let top_singles: Vec<(String, f64)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.singles_rating + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + ORDER BY p.singles_rating DESC LIMIT 5"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let top_doubles: Vec<(String, f64)> = sqlx::query_as( + r#"SELECT DISTINCT p.name, p.doubles_rating + FROM players p + JOIN match_participants mp ON p.id = mp.player_id + ORDER BY p.doubles_rating DESC LIMIT 5"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let singles_list: String = top_singles.iter().enumerate() + .map(|(i, (name, rating))| { + let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; + format!("
{}{}. {} - {:.0}
", medal, i+1, name, rating) + }) + .collect(); + + let doubles_list: String = top_doubles.iter().enumerate() + .map(|(i, (name, rating))| { + let medal = match i { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }; + format!("
{}{}. {} - {:.0}
", medal, i+1, name, rating) + }) + .collect(); + + // Chart data + let singles_history: Vec<(i64, String, String, f64)> = sqlx::query_as( + r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, + p.name, mp.rating_after + FROM matches m + JOIN match_participants mp ON m.id = mp.match_id + JOIN players p ON mp.player_id = p.id + WHERE date(m.timestamp) = ? AND m.match_type = 'singles' + ORDER BY m.timestamp, p.name"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let doubles_history: Vec<(i64, String, String, f64)> = sqlx::query_as( + r#"SELECT m.id, strftime('%H:%M', datetime(m.timestamp, '-5 hours')) as time_str, + p.name, mp.rating_after + FROM matches m + JOIN match_participants mp ON m.id = mp.match_id + JOIN players p ON mp.player_id = p.id + WHERE date(m.timestamp) = ? AND m.match_type = 'doubles' + ORDER BY m.timestamp, p.name"# + ) + .bind(&target_date) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let colors = ["#003594", "#FFB81C", "#dc3545", "#28a745", "#6f42c1", "#fd7e14", "#20c997", "#e83e8c"]; + + fn build_chart_data_public(history: &[(i64, String, String, f64)], colors: &[&str]) -> (String, String) { + let mut match_order: Vec<(i64, String)> = Vec::new(); + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + let mut player_ratings: std::collections::HashMap<(String, i64), f64> = std::collections::HashMap::new(); + let mut all_players: std::collections::HashSet = std::collections::HashSet::new(); + + for (match_id, time_str, name, rating) in history { + if !seen.contains(match_id) { + match_order.push((*match_id, time_str.clone())); + seen.insert(*match_id); + } + player_ratings.insert((name.clone(), *match_id), *rating); + all_players.insert(name.clone()); + } + + let mut sorted_players: Vec = all_players.into_iter().collect(); + sorted_players.sort(); + + let mut datasets = String::new(); + for (i, name) in sorted_players.iter().enumerate() { + let color = colors[i % colors.len()]; + let mut last = 1500.0_f64; + let points: String = match_order.iter() + .map(|(mid, _)| { + if let Some(r) = player_ratings.get(&(name.clone(), *mid)) { last = *r; } + format!("{:.1}", last) + }) + .collect::>() + .join(","); + datasets.push_str(&format!( + r#"{{"label":"{}","data":[{}],"borderColor":"{}","backgroundColor":"{}","tension":0.3,"fill":false}},"#, + name, points, color, color + )); + } + + let labels: String = match_order.iter() + .map(|(_, t)| format!("\"{}\"", t)) + .collect::>() + .join(","); + + (labels, datasets) + } + + let (singles_labels, singles_datasets) = build_chart_data_public(&singles_history, &colors); + let (doubles_labels, doubles_datasets) = build_chart_data_public(&doubles_history, &colors); + + // Partner synergy heatmap + let synergy_data: Vec<(String, String, i64, i64)> = sqlx::query_as( + r#"SELECT p1.name as player1, p2.name as player2, + COUNT(*) as games_together, + SUM(CASE WHEN + (mp1.team = 1 AND m.team1_score > m.team2_score) OR + (mp1.team = 2 AND m.team2_score > m.team1_score) + THEN 1 ELSE 0 END) as wins_together + FROM match_participants mp1 + JOIN match_participants mp2 ON mp1.match_id = mp2.match_id + AND mp1.player_id < mp2.player_id AND mp1.team = mp2.team + JOIN players p1 ON mp1.player_id = p1.id + JOIN players p2 ON mp2.player_id = p2.id + JOIN matches m ON mp1.match_id = m.id + WHERE m.match_type = 'doubles' + GROUP BY p1.name, p2.name"# + ) + .fetch_all(&state.pool) + .await + .unwrap_or_default(); + + let mut all_players: Vec = synergy_data.iter() + .flat_map(|(p1, p2, _, _)| vec![p1.clone(), p2.clone()]) + .collect::>() + .into_iter() + .collect(); + all_players.sort(); + + let mut synergy_matrix: std::collections::HashMap<(String, String), f64> = std::collections::HashMap::new(); + for (p1, p2, games, wins) in &synergy_data { + let win_rate = if *games > 0 { *wins as f64 / *games as f64 } else { 0.0 }; + synergy_matrix.insert((p1.clone(), p2.clone()), win_rate); + synergy_matrix.insert((p2.clone(), p1.clone()), win_rate); + } + + let mut heatmap_html = format!( + "
", + all_players.len() + ); + heatmap_html.push_str("
"); + for p in &all_players { + heatmap_html.push_str(&format!( + "
{}
", + p.split_whitespace().next().unwrap_or(p) + )); + } + for p1 in &all_players { + heatmap_html.push_str(&format!( + "
{}
", + p1.split_whitespace().next().unwrap_or(p1) + )); + for p2 in &all_players { + if p1 == p2 { + heatmap_html.push_str("
"); + } else if let Some(win_rate) = synergy_matrix.get(&(p1.clone(), p2.clone())) { + let (r, g, b) = if *win_rate >= 0.5 { + let t = (*win_rate - 0.5) * 2.0; + (((1.0 - t) * 255.0) as u8, 200, ((1.0 - t) * 255.0) as u8) + } else { + let t = *win_rate * 2.0; + (255, ((t) * 200.0) as u8, ((t) * 255.0) as u8) + }; + heatmap_html.push_str(&format!( + "
{:.0}%
", + r, g, b, p1, p2, win_rate * 100.0, win_rate * 100.0 + )); + } else { + heatmap_html.push_str("
"); + } + } + } + heatmap_html.push_str("
"); + + let no_matches_msg = if matches.is_empty() { + "

No matches recorded for this date.

" + } else { "" }; + + let html = format!(r#" + + + + + + Pickleball Daily Summary - {} + + + + +
+

🏓 Pickleball Daily Summary

+ +
+ ← Prev + {} + Next → +
+ + {} + +
+
+
{}
+
Matches
+
+
+
{}
+
Players
+
+
+ +

🎾 Match Results

+ + + + + {} +
TimeTypeTeam 1ScoreTeam 2
+ +

📈 Player Performance

+ + + + + {} +
PlayerSingles ΔDoubles ΔTotal
+ +

📈 ELO Journey

+
+ +
+
+ +
+ + +

📊 Current Leaderboard

+
+
+

🎾 Singles

+ {} +
+
+

👥 Doubles

+ {} +
+
+ +

🤝 Partner Synergy

+

Win rate when partnered together (all-time doubles)

+ {} + + +
+ + + "#, + target_date, + // Prev date + (chrono::NaiveDate::parse_from_str(&target_date, "%Y-%m-%d") + .unwrap_or_else(|_| chrono::Local::now().date_naive()) - chrono::Duration::days(1)) + .format("%Y-%m-%d"), + target_date, + // Next date + (chrono::NaiveDate::parse_from_str(&target_date, "%Y-%m-%d") + .unwrap_or_else(|_| chrono::Local::now().date_naive()) + chrono::Duration::days(1)) + .format("%Y-%m-%d"), + no_matches_msg, + matches.len(), + player_rating_changes.len(), + matches_html, + players_html, + singles_labels, singles_labels, singles_datasets, + doubles_labels, doubles_labels, doubles_datasets, + singles_list, + doubles_list, + heatmap_html + ); + + Html(html) +} diff --git a/src/simple_demo.rs b/src/simple_demo.rs index ce7ef89..302f355 100644 --- a/src/simple_demo.rs +++ b/src/simple_demo.rs @@ -70,14 +70,18 @@ fn run_session(players: &mut [Player], session_num: usize, num_matches: usize) { &[players[p2_idx].true_skill], ); - // Calculate outcomes with score weighting + // Calculate outcomes with performance-based weighting let p1_outcome = if team1_score > team2_score { - calculate_weighted_score(1.0, team1_score, team2_score) + calculate_weighted_score(players[p1_idx].singles.rating, players[p2_idx].singles.rating, team1_score, team2_score) } else { - calculate_weighted_score(0.0, team2_score, team1_score) + calculate_weighted_score(players[p1_idx].singles.rating, players[p2_idx].singles.rating, team1_score, team2_score) }; - let p2_outcome = 1.0 - p1_outcome; + let p2_outcome = if team1_score > team2_score { + calculate_weighted_score(players[p2_idx].singles.rating, players[p1_idx].singles.rating, team2_score, team1_score) + } else { + calculate_weighted_score(players[p2_idx].singles.rating, players[p1_idx].singles.rating, team2_score, team1_score) + }; // Update ratings players[p1_idx].singles = calc.update_rating( @@ -111,9 +115,9 @@ fn run_session(players: &mut [Player], session_num: usize, num_matches: usize) { // Update team 1 for &idx in &team1_indices { let outcome = if team1_won { - calculate_weighted_score(1.0, team1_score, team2_score) + calculate_weighted_score(players[idx].doubles.rating, team2_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, team1_score, team2_score) } else { - calculate_weighted_score(0.0, team2_score, team1_score) + calculate_weighted_score(players[idx].doubles.rating, team2_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, team1_score, team2_score) }; let avg_opponent = crate::glicko::GlickoRating { @@ -131,9 +135,9 @@ fn run_session(players: &mut [Player], session_num: usize, num_matches: usize) { // Update team 2 for &idx in &team2_indices { let outcome = if !team1_won { - calculate_weighted_score(1.0, team2_score, team1_score) + calculate_weighted_score(players[idx].doubles.rating, team1_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, team2_score, team1_score) } else { - calculate_weighted_score(0.0, team1_score, team2_score) + calculate_weighted_score(players[idx].doubles.rating, team1_indices.iter().map(|&i| players[i].doubles.rating).sum::() / 2.0, team2_score, team1_score) }; let avg_opponent = crate::glicko::GlickoRating {