dubs-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survivor Admin Controller
|
|
3
|
+
* Admin operations for March Madness Survivor Pool
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Generate random bracket (for testing)
|
|
7
|
+
* - Manual team selection (for real Selection Sunday)
|
|
8
|
+
* - Simulate game results
|
|
9
|
+
* - Advance rounds
|
|
10
|
+
* - Manage deadlines
|
|
11
|
+
* - Track eliminations
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { pool } = require('../services/db');
|
|
15
|
+
const { NCAA_TEAMS, CONFERENCES } = require('../data/ncaaTeams');
|
|
16
|
+
|
|
17
|
+
const REGIONS = ['SOUTH', 'WEST', 'EAST', 'MIDWEST'];
|
|
18
|
+
const SEEDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
|
19
|
+
|
|
20
|
+
const ROUND_NAMES = {
|
|
21
|
+
1: 'First Four',
|
|
22
|
+
2: 'Round of 64',
|
|
23
|
+
3: 'Round of 32',
|
|
24
|
+
4: 'Sweet 16',
|
|
25
|
+
5: 'Elite 8',
|
|
26
|
+
6: 'Final Four',
|
|
27
|
+
7: 'Championship'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all NCAA teams for selection
|
|
32
|
+
*/
|
|
33
|
+
function getAllTeams() {
|
|
34
|
+
return {
|
|
35
|
+
teams: NCAA_TEAMS,
|
|
36
|
+
conferences: CONFERENCES,
|
|
37
|
+
count: NCAA_TEAMS.length
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a random 68-team bracket
|
|
43
|
+
* Selects 68 random teams from the 364 D1 teams and assigns them to regions/seeds
|
|
44
|
+
*/
|
|
45
|
+
async function generateRandomBracket(poolId) {
|
|
46
|
+
// Shuffle and pick 68 teams
|
|
47
|
+
const shuffled = [...NCAA_TEAMS].sort(() => Math.random() - 0.5);
|
|
48
|
+
const selected = shuffled.slice(0, 68);
|
|
49
|
+
|
|
50
|
+
// Assign to regions and seeds
|
|
51
|
+
// 16 teams per region = 64 teams + 4 First Four teams
|
|
52
|
+
const bracket = [];
|
|
53
|
+
let teamIndex = 0;
|
|
54
|
+
|
|
55
|
+
for (const region of REGIONS) {
|
|
56
|
+
for (const seed of SEEDS) {
|
|
57
|
+
if (teamIndex < 64) {
|
|
58
|
+
bracket.push({
|
|
59
|
+
team: selected[teamIndex],
|
|
60
|
+
region,
|
|
61
|
+
seed,
|
|
62
|
+
isFirstFour: false
|
|
63
|
+
});
|
|
64
|
+
teamIndex++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// First Four teams (4 extra teams for play-in games)
|
|
70
|
+
// In real tournament, First Four involves 4 matchups of the lowest seeds
|
|
71
|
+
for (let i = 64; i < 68; i++) {
|
|
72
|
+
bracket.push({
|
|
73
|
+
team: selected[i],
|
|
74
|
+
region: REGIONS[i - 64],
|
|
75
|
+
seed: 16, // First Four are typically 16 seeds or 11/12 seeds
|
|
76
|
+
isFirstFour: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Clear existing games for this pool
|
|
81
|
+
await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
|
|
82
|
+
|
|
83
|
+
// Generate Round 1 matchups (Round of 64)
|
|
84
|
+
// 1 vs 16, 8 vs 9, 5 vs 12, 4 vs 13, 6 vs 11, 3 vs 14, 7 vs 10, 2 vs 15
|
|
85
|
+
const seedMatchups = [
|
|
86
|
+
[1, 16], [8, 9], [5, 12], [4, 13], [6, 11], [3, 14], [7, 10], [2, 15]
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const games = [];
|
|
90
|
+
let gameId = 1;
|
|
91
|
+
|
|
92
|
+
for (const region of REGIONS) {
|
|
93
|
+
const regionTeams = bracket.filter(t => t.region === region && !t.isFirstFour);
|
|
94
|
+
|
|
95
|
+
for (const [seed1, seed2] of seedMatchups) {
|
|
96
|
+
const team1 = regionTeams.find(t => t.seed === seed1);
|
|
97
|
+
const team2 = regionTeams.find(t => t.seed === seed2);
|
|
98
|
+
|
|
99
|
+
if (team1 && team2) {
|
|
100
|
+
games.push({
|
|
101
|
+
poolId,
|
|
102
|
+
round: 2, // Round of 64
|
|
103
|
+
espnGameId: `sim-r64-${region.toLowerCase()}-${gameId}`,
|
|
104
|
+
region,
|
|
105
|
+
team1Id: team1.team.id,
|
|
106
|
+
team1Name: team1.team.name,
|
|
107
|
+
team1Seed: team1.seed,
|
|
108
|
+
team1Logo: team1.team.logo,
|
|
109
|
+
team2Id: team2.team.id,
|
|
110
|
+
team2Name: team2.team.name,
|
|
111
|
+
team2Seed: team2.seed,
|
|
112
|
+
team2Logo: team2.team.logo,
|
|
113
|
+
status: 'scheduled'
|
|
114
|
+
});
|
|
115
|
+
gameId++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Insert games
|
|
121
|
+
for (const game of games) {
|
|
122
|
+
await pool.query(
|
|
123
|
+
`INSERT INTO survivor_tournament_games
|
|
124
|
+
(pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
|
|
125
|
+
team2_id, team2_name, team2_seed, team2_logo, status)
|
|
126
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
127
|
+
[
|
|
128
|
+
game.poolId, game.round, game.espnGameId, game.region,
|
|
129
|
+
game.team1Id, game.team1Name, game.team1Seed, game.team1Logo,
|
|
130
|
+
game.team2Id, game.team2Name, game.team2Seed, game.team2Logo,
|
|
131
|
+
game.status
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Update pool status to active and set to Round 2 (Round of 64)
|
|
137
|
+
await pool.query(
|
|
138
|
+
`UPDATE survivor_pools
|
|
139
|
+
SET status = 'active', current_round = 2, updated_at = NOW()
|
|
140
|
+
WHERE id = $1`,
|
|
141
|
+
[poolId]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
message: 'Random bracket generated',
|
|
146
|
+
teamsSelected: 68,
|
|
147
|
+
gamesCreated: games.length,
|
|
148
|
+
bracket: bracket.map(b => ({
|
|
149
|
+
team: b.team.name,
|
|
150
|
+
region: b.region,
|
|
151
|
+
seed: b.seed
|
|
152
|
+
}))
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Set bracket manually (for real Selection Sunday)
|
|
158
|
+
* teams: Array of { teamId, region, seed }
|
|
159
|
+
*/
|
|
160
|
+
async function setManualBracket(poolId, teams) {
|
|
161
|
+
if (teams.length !== 68 && teams.length !== 64) {
|
|
162
|
+
throw new Error(`Expected 64 or 68 teams, got ${teams.length}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Validate teams exist
|
|
166
|
+
const teamMap = new Map(NCAA_TEAMS.map(t => [t.id, t]));
|
|
167
|
+
const bracket = teams.map(t => {
|
|
168
|
+
const team = teamMap.get(t.teamId);
|
|
169
|
+
if (!team) {
|
|
170
|
+
throw new Error(`Unknown team ID: ${t.teamId}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
team,
|
|
174
|
+
region: t.region,
|
|
175
|
+
seed: t.seed
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Clear existing games
|
|
180
|
+
await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
|
|
181
|
+
|
|
182
|
+
// Generate matchups (same logic as random)
|
|
183
|
+
const seedMatchups = [
|
|
184
|
+
[1, 16], [8, 9], [5, 12], [4, 13], [6, 11], [3, 14], [7, 10], [2, 15]
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const games = [];
|
|
188
|
+
let gameId = 1;
|
|
189
|
+
|
|
190
|
+
for (const region of REGIONS) {
|
|
191
|
+
const regionTeams = bracket.filter(t => t.region === region);
|
|
192
|
+
|
|
193
|
+
for (const [seed1, seed2] of seedMatchups) {
|
|
194
|
+
const team1 = regionTeams.find(t => t.seed === seed1);
|
|
195
|
+
const team2 = regionTeams.find(t => t.seed === seed2);
|
|
196
|
+
|
|
197
|
+
if (team1 && team2) {
|
|
198
|
+
games.push({
|
|
199
|
+
poolId,
|
|
200
|
+
round: 2,
|
|
201
|
+
espnGameId: `man-r64-${region.toLowerCase()}-${gameId}`,
|
|
202
|
+
region,
|
|
203
|
+
team1Id: team1.team.id,
|
|
204
|
+
team1Name: team1.team.name,
|
|
205
|
+
team1Seed: team1.seed,
|
|
206
|
+
team1Logo: team1.team.logo,
|
|
207
|
+
team2Id: team2.team.id,
|
|
208
|
+
team2Name: team2.team.name,
|
|
209
|
+
team2Seed: team2.seed,
|
|
210
|
+
team2Logo: team2.team.logo,
|
|
211
|
+
status: 'scheduled'
|
|
212
|
+
});
|
|
213
|
+
gameId++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Insert games
|
|
219
|
+
for (const game of games) {
|
|
220
|
+
await pool.query(
|
|
221
|
+
`INSERT INTO survivor_tournament_games
|
|
222
|
+
(pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
|
|
223
|
+
team2_id, team2_name, team2_seed, team2_logo, status)
|
|
224
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
225
|
+
[
|
|
226
|
+
game.poolId, game.round, game.espnGameId, game.region,
|
|
227
|
+
game.team1Id, game.team1Name, game.team1Seed, game.team1Logo,
|
|
228
|
+
game.team2Id, game.team2Name, game.team2Seed, game.team2Logo,
|
|
229
|
+
game.status
|
|
230
|
+
]
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Update pool status
|
|
235
|
+
await pool.query(
|
|
236
|
+
`UPDATE survivor_pools
|
|
237
|
+
SET status = 'active', current_round = 2, updated_at = NOW()
|
|
238
|
+
WHERE id = $1`,
|
|
239
|
+
[poolId]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
message: 'Manual bracket set',
|
|
244
|
+
teamsSelected: teams.length,
|
|
245
|
+
gamesCreated: games.length
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set round deadline
|
|
251
|
+
* minutes: Number of minutes from now
|
|
252
|
+
*/
|
|
253
|
+
async function setDeadline(poolId, minutes) {
|
|
254
|
+
const deadline = new Date(Date.now() + minutes * 60 * 1000);
|
|
255
|
+
|
|
256
|
+
await pool.query(
|
|
257
|
+
`UPDATE survivor_pools
|
|
258
|
+
SET round_deadline = $2, updated_at = NOW()
|
|
259
|
+
WHERE id = $1`,
|
|
260
|
+
[poolId, deadline]
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
message: `Deadline set to ${minutes} minutes from now`,
|
|
265
|
+
deadline: deadline.toISOString(),
|
|
266
|
+
deadlineLocal: deadline.toLocaleString()
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clear deadline (no deadline)
|
|
272
|
+
*/
|
|
273
|
+
async function clearDeadline(poolId) {
|
|
274
|
+
await pool.query(
|
|
275
|
+
`UPDATE survivor_pools
|
|
276
|
+
SET round_deadline = NULL, updated_at = NOW()
|
|
277
|
+
WHERE id = $1`,
|
|
278
|
+
[poolId]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return { message: 'Deadline cleared' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Simulate current round results
|
|
286
|
+
* mode: 'chalk' (favorites win), 'random' (50/50), 'chaos' (upsets likely), 'manual'
|
|
287
|
+
*/
|
|
288
|
+
async function simulateRound(poolId, mode = 'random', manualResults = null) {
|
|
289
|
+
// Get current round
|
|
290
|
+
const poolResult = await pool.query(
|
|
291
|
+
'SELECT current_round FROM survivor_pools WHERE id = $1',
|
|
292
|
+
[poolId]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (poolResult.rows.length === 0) {
|
|
296
|
+
throw new Error('Pool not found');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentRound = poolResult.rows[0].current_round;
|
|
300
|
+
|
|
301
|
+
// Get games for current round
|
|
302
|
+
const gamesResult = await pool.query(
|
|
303
|
+
`SELECT * FROM survivor_tournament_games
|
|
304
|
+
WHERE pool_id = $1 AND round = $2 AND winner_id IS NULL
|
|
305
|
+
ORDER BY id`,
|
|
306
|
+
[poolId, currentRound]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const games = gamesResult.rows;
|
|
310
|
+
|
|
311
|
+
if (games.length === 0) {
|
|
312
|
+
throw new Error('No unresolved games in current round');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const results = [];
|
|
316
|
+
|
|
317
|
+
for (const game of games) {
|
|
318
|
+
let winnerId;
|
|
319
|
+
let winnerName;
|
|
320
|
+
|
|
321
|
+
if (mode === 'manual' && manualResults) {
|
|
322
|
+
// Use provided results
|
|
323
|
+
const result = manualResults.find(r => r.gameId === game.id || r.espnGameId === game.espn_game_id);
|
|
324
|
+
if (result) {
|
|
325
|
+
winnerId = result.winnerId;
|
|
326
|
+
winnerName = winnerId === game.team1_id ? game.team1_name : game.team2_name;
|
|
327
|
+
} else {
|
|
328
|
+
continue; // Skip if no manual result provided
|
|
329
|
+
}
|
|
330
|
+
} else if (mode === 'chalk') {
|
|
331
|
+
// Lower seed (favorite) wins
|
|
332
|
+
winnerId = game.team1_seed <= game.team2_seed ? game.team1_id : game.team2_id;
|
|
333
|
+
winnerName = game.team1_seed <= game.team2_seed ? game.team1_name : game.team2_name;
|
|
334
|
+
} else if (mode === 'chaos') {
|
|
335
|
+
// 70% chance of upset
|
|
336
|
+
const upset = Math.random() < 0.7;
|
|
337
|
+
if (upset) {
|
|
338
|
+
winnerId = game.team1_seed > game.team2_seed ? game.team1_id : game.team2_id;
|
|
339
|
+
winnerName = game.team1_seed > game.team2_seed ? game.team1_name : game.team2_name;
|
|
340
|
+
} else {
|
|
341
|
+
winnerId = game.team1_seed <= game.team2_seed ? game.team1_id : game.team2_id;
|
|
342
|
+
winnerName = game.team1_seed <= game.team2_seed ? game.team1_name : game.team2_name;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Random 50/50
|
|
346
|
+
const team1Wins = Math.random() < 0.5;
|
|
347
|
+
winnerId = team1Wins ? game.team1_id : game.team2_id;
|
|
348
|
+
winnerName = team1Wins ? game.team1_name : game.team2_name;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Update game with winner
|
|
352
|
+
await pool.query(
|
|
353
|
+
`UPDATE survivor_tournament_games
|
|
354
|
+
SET winner_id = $2, status = 'final', updated_at = NOW()
|
|
355
|
+
WHERE id = $1`,
|
|
356
|
+
[game.id, winnerId]
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
results.push({
|
|
360
|
+
gameId: game.id,
|
|
361
|
+
matchup: `${game.team1_name} (${game.team1_seed}) vs ${game.team2_name} (${game.team2_seed})`,
|
|
362
|
+
winner: winnerName,
|
|
363
|
+
upset: (winnerId === game.team1_id && game.team1_seed > game.team2_seed) ||
|
|
364
|
+
(winnerId === game.team2_id && game.team2_seed > game.team1_seed)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
message: `Simulated ${results.length} games with mode: ${mode}`,
|
|
370
|
+
round: currentRound,
|
|
371
|
+
roundName: ROUND_NAMES[currentRound],
|
|
372
|
+
results,
|
|
373
|
+
upsets: results.filter(r => r.upset).length
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Process eliminations for current round
|
|
379
|
+
* Checks all survivor picks and eliminates those who picked losing teams
|
|
380
|
+
*/
|
|
381
|
+
async function processEliminations(poolId) {
|
|
382
|
+
// Get current round
|
|
383
|
+
const poolResult = await pool.query(
|
|
384
|
+
'SELECT current_round FROM survivor_pools WHERE id = $1',
|
|
385
|
+
[poolId]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const currentRound = poolResult.rows[0].current_round;
|
|
389
|
+
|
|
390
|
+
// Get all games for current round that are final
|
|
391
|
+
const gamesResult = await pool.query(
|
|
392
|
+
`SELECT * FROM survivor_tournament_games
|
|
393
|
+
WHERE pool_id = $1 AND round = $2 AND status = 'final'`,
|
|
394
|
+
[poolId, currentRound]
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const games = gamesResult.rows;
|
|
398
|
+
const winnerIds = new Set(games.map(g => g.winner_id));
|
|
399
|
+
const loserIds = new Set();
|
|
400
|
+
|
|
401
|
+
games.forEach(g => {
|
|
402
|
+
if (g.winner_id === g.team1_id) {
|
|
403
|
+
loserIds.add(g.team2_id);
|
|
404
|
+
} else {
|
|
405
|
+
loserIds.add(g.team1_id);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Update survivor picks for this round
|
|
410
|
+
// Mark picks as 'won' or 'lost'
|
|
411
|
+
await pool.query(
|
|
412
|
+
`UPDATE survivor_picks sp
|
|
413
|
+
SET result = 'won', updated_at = NOW()
|
|
414
|
+
FROM survivor_entries se
|
|
415
|
+
WHERE sp.entry_id = se.id
|
|
416
|
+
AND se.pool_id = $1
|
|
417
|
+
AND sp.round = $2
|
|
418
|
+
AND sp.team_id = ANY($3::text[])`,
|
|
419
|
+
[poolId, currentRound, Array.from(winnerIds)]
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
await pool.query(
|
|
423
|
+
`UPDATE survivor_picks sp
|
|
424
|
+
SET result = 'lost', updated_at = NOW()
|
|
425
|
+
FROM survivor_entries se
|
|
426
|
+
WHERE sp.entry_id = se.id
|
|
427
|
+
AND se.pool_id = $1
|
|
428
|
+
AND sp.round = $2
|
|
429
|
+
AND sp.team_id = ANY($3::text[])`,
|
|
430
|
+
[poolId, currentRound, Array.from(loserIds)]
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Eliminate entries whose picks lost
|
|
434
|
+
const eliminatedResult = await pool.query(
|
|
435
|
+
`UPDATE survivor_entries se
|
|
436
|
+
SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
|
|
437
|
+
FROM survivor_picks sp
|
|
438
|
+
WHERE se.id = sp.entry_id
|
|
439
|
+
AND se.pool_id = $1
|
|
440
|
+
AND sp.round = $2
|
|
441
|
+
AND sp.result = 'lost'
|
|
442
|
+
AND se.is_alive = true
|
|
443
|
+
RETURNING se.id, se.user_id`,
|
|
444
|
+
[poolId, currentRound]
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Also eliminate entries that didn't make a pick (no-pick elimination)
|
|
448
|
+
// First, let's see who is alive and didn't pick
|
|
449
|
+
const checkNoPickers = await pool.query(
|
|
450
|
+
`SELECT se.id, se.user_id, u.username
|
|
451
|
+
FROM survivor_entries se
|
|
452
|
+
LEFT JOIN users u ON u.id = se.user_id
|
|
453
|
+
WHERE se.pool_id = $1
|
|
454
|
+
AND se.is_alive = true
|
|
455
|
+
AND NOT EXISTS (
|
|
456
|
+
SELECT 1 FROM survivor_picks sp
|
|
457
|
+
WHERE sp.entry_id = se.id AND sp.round = $2
|
|
458
|
+
)`,
|
|
459
|
+
[poolId, currentRound]
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
console.log(`[Survivor] Round ${currentRound}: ${checkNoPickers.rows.length} alive entries without picks:`,
|
|
463
|
+
checkNoPickers.rows.map(r => ({ entryId: r.id, userId: r.user_id, username: r.username })));
|
|
464
|
+
|
|
465
|
+
const noPickers = await pool.query(
|
|
466
|
+
`UPDATE survivor_entries se
|
|
467
|
+
SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
|
|
468
|
+
WHERE se.pool_id = $1
|
|
469
|
+
AND se.is_alive = true
|
|
470
|
+
AND NOT EXISTS (
|
|
471
|
+
SELECT 1 FROM survivor_picks sp
|
|
472
|
+
WHERE sp.entry_id = se.id AND sp.round = $2
|
|
473
|
+
)
|
|
474
|
+
RETURNING se.id, se.user_id`,
|
|
475
|
+
[poolId, currentRound]
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
console.log(`[Survivor] Round ${currentRound}: Eliminated ${noPickers.rowCount} entries for no-pick`);
|
|
479
|
+
|
|
480
|
+
// Get counts
|
|
481
|
+
const countResult = await pool.query(
|
|
482
|
+
`SELECT
|
|
483
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = true) as alive,
|
|
484
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = false) as eliminated,
|
|
485
|
+
(SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1) as total`,
|
|
486
|
+
[poolId]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const remainingAlive = parseInt(countResult.rows[0].alive);
|
|
490
|
+
const totalEntries = parseInt(countResult.rows[0].total);
|
|
491
|
+
|
|
492
|
+
// Check if everyone is eliminated - pool goes to house
|
|
493
|
+
if (remainingAlive === 0 && totalEntries > 0) {
|
|
494
|
+
await pool.query(
|
|
495
|
+
`UPDATE survivor_pools
|
|
496
|
+
SET status = 'complete',
|
|
497
|
+
winner_type = 'house',
|
|
498
|
+
completed_at = NOW(),
|
|
499
|
+
updated_at = NOW()
|
|
500
|
+
WHERE id = $1`,
|
|
501
|
+
[poolId]
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
message: 'ALL PLAYERS ELIMINATED - Pool goes to house!',
|
|
506
|
+
round: currentRound,
|
|
507
|
+
roundName: ROUND_NAMES[currentRound],
|
|
508
|
+
eliminatedByLoss: eliminatedResult.rowCount,
|
|
509
|
+
eliminatedByNoPick: noPickers.rowCount,
|
|
510
|
+
totalEliminated: eliminatedResult.rowCount + noPickers.rowCount,
|
|
511
|
+
remainingAlive: 0,
|
|
512
|
+
allEliminated: true,
|
|
513
|
+
outcome: 'house_wins',
|
|
514
|
+
note: 'No survivors remain. The entire pot goes to the house.'
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
message: 'Eliminations processed',
|
|
520
|
+
round: currentRound,
|
|
521
|
+
roundName: ROUND_NAMES[currentRound],
|
|
522
|
+
eliminatedByLoss: eliminatedResult.rowCount,
|
|
523
|
+
eliminatedByNoPick: noPickers.rowCount,
|
|
524
|
+
totalEliminated: eliminatedResult.rowCount + noPickers.rowCount,
|
|
525
|
+
remainingAlive,
|
|
526
|
+
allEliminated: false
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Advance to next round
|
|
532
|
+
* Creates matchups for next round from winners
|
|
533
|
+
*/
|
|
534
|
+
async function advanceRound(poolId) {
|
|
535
|
+
// Get current round
|
|
536
|
+
const poolResult = await pool.query(
|
|
537
|
+
'SELECT current_round FROM survivor_pools WHERE id = $1',
|
|
538
|
+
[poolId]
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const currentRound = poolResult.rows[0].current_round;
|
|
542
|
+
const nextRound = currentRound + 1;
|
|
543
|
+
|
|
544
|
+
if (nextRound > 7) {
|
|
545
|
+
// Tournament complete - survivors win!
|
|
546
|
+
// Get count of remaining survivors
|
|
547
|
+
const survivorCount = await pool.query(
|
|
548
|
+
`SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1 AND is_alive = true`,
|
|
549
|
+
[poolId]
|
|
550
|
+
);
|
|
551
|
+
const numSurvivors = parseInt(survivorCount.rows[0].count);
|
|
552
|
+
|
|
553
|
+
console.log(`[Survivor] Tournament complete! ${numSurvivors} survivors remaining`);
|
|
554
|
+
|
|
555
|
+
await pool.query(
|
|
556
|
+
`UPDATE survivor_pools
|
|
557
|
+
SET status = 'complete',
|
|
558
|
+
winner_type = 'survivors',
|
|
559
|
+
completed_at = NOW(),
|
|
560
|
+
updated_at = NOW()
|
|
561
|
+
WHERE id = $1`,
|
|
562
|
+
[poolId]
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
message: `Tournament complete! ${numSurvivors} survivor(s) win!`,
|
|
567
|
+
status: 'complete',
|
|
568
|
+
winner_type: 'survivors',
|
|
569
|
+
survivorCount: numSurvivors
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check if there are any games in current round
|
|
574
|
+
const currentGamesResult = await pool.query(
|
|
575
|
+
`SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'final') as resolved
|
|
576
|
+
FROM survivor_tournament_games WHERE pool_id = $1 AND round = $2`,
|
|
577
|
+
[poolId, currentRound]
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
const totalGames = parseInt(currentGamesResult.rows[0].total);
|
|
581
|
+
const resolvedGames = parseInt(currentGamesResult.rows[0].resolved);
|
|
582
|
+
|
|
583
|
+
if (totalGames === 0) {
|
|
584
|
+
throw new Error(`No games exist for current round (${ROUND_NAMES[currentRound]}). Seed the bracket first.`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (resolvedGames === 0) {
|
|
588
|
+
throw new Error(`No games have been resolved in ${ROUND_NAMES[currentRound]}. Simulate the round first.`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (resolvedGames < totalGames) {
|
|
592
|
+
throw new Error(`Only ${resolvedGames}/${totalGames} games resolved in ${ROUND_NAMES[currentRound]}. Resolve all games before advancing.`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Get winners from current round
|
|
596
|
+
const winnersResult = await pool.query(
|
|
597
|
+
`SELECT * FROM survivor_tournament_games
|
|
598
|
+
WHERE pool_id = $1 AND round = $2 AND status = 'final'
|
|
599
|
+
ORDER BY region, id`,
|
|
600
|
+
[poolId, currentRound]
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const winners = winnersResult.rows.map(g => ({
|
|
604
|
+
teamId: g.winner_id,
|
|
605
|
+
teamName: g.winner_id === g.team1_id ? g.team1_name : g.team2_name,
|
|
606
|
+
teamSeed: g.winner_id === g.team1_id ? g.team1_seed : g.team2_seed,
|
|
607
|
+
teamLogo: g.winner_id === g.team1_id ? g.team1_logo : g.team2_logo,
|
|
608
|
+
region: g.region
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
// Create next round matchups
|
|
612
|
+
const games = [];
|
|
613
|
+
|
|
614
|
+
if (nextRound <= 5) {
|
|
615
|
+
// Regional rounds (through Elite 8)
|
|
616
|
+
for (const region of REGIONS) {
|
|
617
|
+
const regionWinners = winners.filter(w => w.region === region);
|
|
618
|
+
|
|
619
|
+
// Pair winners
|
|
620
|
+
for (let i = 0; i < regionWinners.length; i += 2) {
|
|
621
|
+
if (regionWinners[i + 1]) {
|
|
622
|
+
games.push({
|
|
623
|
+
poolId,
|
|
624
|
+
round: nextRound,
|
|
625
|
+
espnGameId: `sim-r${nextRound}-${region.toLowerCase()}-${i / 2 + 1}`,
|
|
626
|
+
region,
|
|
627
|
+
team1: regionWinners[i],
|
|
628
|
+
team2: regionWinners[i + 1]
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} else if (nextRound === 6) {
|
|
634
|
+
// Final Four
|
|
635
|
+
// South vs East, West vs Midwest
|
|
636
|
+
const pairings = [
|
|
637
|
+
['SOUTH', 'EAST'],
|
|
638
|
+
['WEST', 'MIDWEST']
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
console.log(`[SurvivorAdmin] Creating Final Four. Winners from Elite 8:`, winners.map(w => ({ team: w.teamName, region: w.region })));
|
|
642
|
+
|
|
643
|
+
for (let i = 0; i < pairings.length; i++) {
|
|
644
|
+
const team1 = winners.find(w => w.region === pairings[i][0]);
|
|
645
|
+
const team2 = winners.find(w => w.region === pairings[i][1]);
|
|
646
|
+
|
|
647
|
+
console.log(`[SurvivorAdmin] Final Four pairing ${i + 1}: ${pairings[i][0]} vs ${pairings[i][1]}`);
|
|
648
|
+
console.log(`[SurvivorAdmin] Team1 (${pairings[i][0]}):`, team1 ? team1.teamName : 'NOT FOUND');
|
|
649
|
+
console.log(`[SurvivorAdmin] Team2 (${pairings[i][1]}):`, team2 ? team2.teamName : 'NOT FOUND');
|
|
650
|
+
|
|
651
|
+
if (team1 && team2) {
|
|
652
|
+
games.push({
|
|
653
|
+
poolId,
|
|
654
|
+
round: nextRound,
|
|
655
|
+
espnGameId: `sim-ff-${i + 1}`,
|
|
656
|
+
region: 'FINAL_FOUR',
|
|
657
|
+
team1,
|
|
658
|
+
team2
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
console.warn(`[SurvivorAdmin] Could not create Final Four game - missing team(s)`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} else if (nextRound === 7) {
|
|
665
|
+
// Championship
|
|
666
|
+
if (winners.length === 2) {
|
|
667
|
+
games.push({
|
|
668
|
+
poolId,
|
|
669
|
+
round: nextRound,
|
|
670
|
+
espnGameId: 'sim-champ',
|
|
671
|
+
region: 'CHAMPIONSHIP',
|
|
672
|
+
team1: winners[0],
|
|
673
|
+
team2: winners[1]
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Validate we have games to create
|
|
679
|
+
if (games.length === 0) {
|
|
680
|
+
throw new Error(`Failed to create matchups for ${ROUND_NAMES[nextRound]}. No valid winner pairings found.`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Insert new games
|
|
684
|
+
for (const game of games) {
|
|
685
|
+
await pool.query(
|
|
686
|
+
`INSERT INTO survivor_tournament_games
|
|
687
|
+
(pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
|
|
688
|
+
team2_id, team2_name, team2_seed, team2_logo, status)
|
|
689
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'scheduled')`,
|
|
690
|
+
[
|
|
691
|
+
game.poolId, game.round, game.espnGameId, game.region,
|
|
692
|
+
game.team1.teamId, game.team1.teamName, game.team1.teamSeed, game.team1.teamLogo,
|
|
693
|
+
game.team2.teamId, game.team2.teamName, game.team2.teamSeed, game.team2.teamLogo
|
|
694
|
+
]
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Update pool to next round and clear deadline
|
|
699
|
+
await pool.query(
|
|
700
|
+
`UPDATE survivor_pools
|
|
701
|
+
SET current_round = $2, round_deadline = NULL, updated_at = NOW()
|
|
702
|
+
WHERE id = $1`,
|
|
703
|
+
[poolId, nextRound]
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
message: `Advanced to ${ROUND_NAMES[nextRound]}`,
|
|
708
|
+
previousRound: currentRound,
|
|
709
|
+
newRound: nextRound,
|
|
710
|
+
roundName: ROUND_NAMES[nextRound],
|
|
711
|
+
gamesCreated: games.length,
|
|
712
|
+
matchups: games.map(g => `${g.team1.teamName} vs ${g.team2.teamName}`)
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Get pool admin dashboard data
|
|
718
|
+
*/
|
|
719
|
+
async function getPoolDashboard(poolId) {
|
|
720
|
+
// Pool info
|
|
721
|
+
const poolResult = await pool.query(
|
|
722
|
+
`SELECT * FROM survivor_pools WHERE id = $1`,
|
|
723
|
+
[poolId]
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (poolResult.rows.length === 0) {
|
|
727
|
+
throw new Error('Pool not found');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const survivorPool = poolResult.rows[0];
|
|
731
|
+
|
|
732
|
+
// Entry counts
|
|
733
|
+
const countsResult = await pool.query(
|
|
734
|
+
`SELECT
|
|
735
|
+
COUNT(*) as total,
|
|
736
|
+
COUNT(*) FILTER (WHERE is_alive = true) as alive,
|
|
737
|
+
COUNT(*) FILTER (WHERE is_alive = false) as eliminated
|
|
738
|
+
FROM survivor_entries WHERE pool_id = $1`,
|
|
739
|
+
[poolId]
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
// Check if bracket exists (any games at all)
|
|
743
|
+
const bracketExistsResult = await pool.query(
|
|
744
|
+
`SELECT COUNT(*) as count FROM survivor_tournament_games WHERE pool_id = $1`,
|
|
745
|
+
[poolId]
|
|
746
|
+
);
|
|
747
|
+
const hasBracket = parseInt(bracketExistsResult.rows[0].count) > 0;
|
|
748
|
+
|
|
749
|
+
// Games for current round
|
|
750
|
+
const gamesResult = await pool.query(
|
|
751
|
+
`SELECT * FROM survivor_tournament_games
|
|
752
|
+
WHERE pool_id = $1 AND round = $2
|
|
753
|
+
ORDER BY region, id`,
|
|
754
|
+
[poolId, survivorPool.current_round]
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
// Picks for current round
|
|
758
|
+
const picksResult = await pool.query(
|
|
759
|
+
`SELECT sp.team_id, sp.team_name, COUNT(*) as pick_count
|
|
760
|
+
FROM survivor_picks sp
|
|
761
|
+
JOIN survivor_entries se ON se.id = sp.entry_id
|
|
762
|
+
WHERE se.pool_id = $1 AND sp.round = $2
|
|
763
|
+
GROUP BY sp.team_id, sp.team_name
|
|
764
|
+
ORDER BY pick_count DESC`,
|
|
765
|
+
[poolId, survivorPool.current_round]
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Entries without picks for current round
|
|
769
|
+
const noPicks = await pool.query(
|
|
770
|
+
`SELECT COUNT(*) as count
|
|
771
|
+
FROM survivor_entries se
|
|
772
|
+
WHERE se.pool_id = $1
|
|
773
|
+
AND se.is_alive = true
|
|
774
|
+
AND NOT EXISTS (
|
|
775
|
+
SELECT 1 FROM survivor_picks sp
|
|
776
|
+
WHERE sp.entry_id = se.id AND sp.round = $2
|
|
777
|
+
)`,
|
|
778
|
+
[poolId, survivorPool.current_round]
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const totalEntries = parseInt(countsResult.rows[0].total);
|
|
782
|
+
const aliveEntries = parseInt(countsResult.rows[0].alive);
|
|
783
|
+
const eliminatedEntries = parseInt(countsResult.rows[0].eliminated);
|
|
784
|
+
|
|
785
|
+
// Determine pool outcome state
|
|
786
|
+
let outcomeState = null;
|
|
787
|
+
if (survivorPool.status === 'complete') {
|
|
788
|
+
if (aliveEntries === 0 && totalEntries > 0) {
|
|
789
|
+
outcomeState = 'house_wins';
|
|
790
|
+
} else if (aliveEntries > 0) {
|
|
791
|
+
outcomeState = 'survivors_win';
|
|
792
|
+
}
|
|
793
|
+
} else if (aliveEntries === 0 && totalEntries > 0 && hasBracket) {
|
|
794
|
+
// Pool should be marked complete but hasn't been yet
|
|
795
|
+
outcomeState = 'pending_house_wins';
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
pool: {
|
|
800
|
+
id: survivorPool.id,
|
|
801
|
+
name: survivorPool.name,
|
|
802
|
+
status: survivorPool.status,
|
|
803
|
+
currentRound: survivorPool.current_round,
|
|
804
|
+
roundName: hasBracket ? ROUND_NAMES[survivorPool.current_round] : 'Pre-Tournament',
|
|
805
|
+
hasBracket,
|
|
806
|
+
deadline: survivorPool.round_deadline,
|
|
807
|
+
deadlinePassed: survivorPool.round_deadline ? new Date() > new Date(survivorPool.round_deadline) : false,
|
|
808
|
+
winnerType: survivorPool.winner_type || null,
|
|
809
|
+
completedAt: survivorPool.completed_at || null
|
|
810
|
+
},
|
|
811
|
+
entries: {
|
|
812
|
+
total: totalEntries,
|
|
813
|
+
alive: aliveEntries,
|
|
814
|
+
eliminated: eliminatedEntries,
|
|
815
|
+
noPick: parseInt(noPicks.rows[0].count)
|
|
816
|
+
},
|
|
817
|
+
outcomeState,
|
|
818
|
+
currentRoundGames: gamesResult.rows.map(g => ({
|
|
819
|
+
id: g.id,
|
|
820
|
+
region: g.region,
|
|
821
|
+
team1: { id: g.team1_id, name: g.team1_name, seed: g.team1_seed, logo: g.team1_logo },
|
|
822
|
+
team2: { id: g.team2_id, name: g.team2_name, seed: g.team2_seed, logo: g.team2_logo },
|
|
823
|
+
winner: g.winner_id,
|
|
824
|
+
status: g.status
|
|
825
|
+
})),
|
|
826
|
+
pickDistribution: picksResult.rows
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Reset pool to initial state
|
|
832
|
+
*/
|
|
833
|
+
async function resetPool(poolId, options = {}) {
|
|
834
|
+
const { deleteEntries = false } = options;
|
|
835
|
+
|
|
836
|
+
// Delete all games
|
|
837
|
+
await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
|
|
838
|
+
|
|
839
|
+
// Delete all picks
|
|
840
|
+
await pool.query(
|
|
841
|
+
`DELETE FROM survivor_picks sp
|
|
842
|
+
USING survivor_entries se
|
|
843
|
+
WHERE sp.entry_id = se.id AND se.pool_id = $1`,
|
|
844
|
+
[poolId]
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
if (deleteEntries) {
|
|
848
|
+
// Full reset: delete all entries
|
|
849
|
+
await pool.query('DELETE FROM survivor_entries WHERE pool_id = $1', [poolId]);
|
|
850
|
+
} else {
|
|
851
|
+
// Soft reset: just reset entries to alive
|
|
852
|
+
await pool.query(
|
|
853
|
+
`UPDATE survivor_entries
|
|
854
|
+
SET is_alive = true, eliminated_at_round = NULL, updated_at = NOW()
|
|
855
|
+
WHERE pool_id = $1`,
|
|
856
|
+
[poolId]
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Reset pool
|
|
861
|
+
await pool.query(
|
|
862
|
+
`UPDATE survivor_pools
|
|
863
|
+
SET status = 'open', current_round = 1, round_deadline = NULL, updated_at = NOW()
|
|
864
|
+
WHERE id = $1`,
|
|
865
|
+
[poolId]
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const message = deleteEntries
|
|
869
|
+
? 'Pool fully reset (all entries deleted)'
|
|
870
|
+
: 'Pool reset to initial state (entries kept)';
|
|
871
|
+
|
|
872
|
+
return { message, entriesDeleted: deleteEntries };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
module.exports = {
|
|
876
|
+
getAllTeams,
|
|
877
|
+
generateRandomBracket,
|
|
878
|
+
setManualBracket,
|
|
879
|
+
setDeadline,
|
|
880
|
+
clearDeadline,
|
|
881
|
+
simulateRound,
|
|
882
|
+
processEliminations,
|
|
883
|
+
advanceRound,
|
|
884
|
+
getPoolDashboard,
|
|
885
|
+
resetPool,
|
|
886
|
+
ROUND_NAMES
|
|
887
|
+
};
|