Created
January 10, 2025 13:53
-
-
Save witscher/e7696f4dda4a8d340dcc4d47e5455b3f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta name="chrome-pdf-print-options" content="headerFooterEnabled=false"> | |
<title>BuilderCards Tournament Plan</title> | |
<!-- Bootstrap 5 CSS --> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<style> | |
@media print { | |
body { | |
margin: 0; | |
padding: 0; | |
font-size: 7pt; /* Smaller base font */ | |
} | |
.container h1 { | |
font-size: 12pt; /* Smaller round headers */ | |
} | |
.round-container h3 { | |
font-size: 10pt; /* Smaller round headers */ | |
} | |
.group-container h5 { | |
font-size: 10pt; /* Smaller group headers */ | |
} | |
.no-print { | |
display: none; | |
} | |
} | |
</style> | |
</head> | |
<body class="bg-light"> | |
<div class="container mt-5"> | |
<div class="card shadow"> | |
<div class="card-body"> | |
<h1 class="card-title text-center mb-2">BuilderCards Tournament Plan</h1> | |
<form> | |
<div class="mb-3 no-print"> | |
<label for="playerNames" class="form-label no-print">Enter Player Names (one per line):</label> | |
<textarea id="playerNames" class="form-control no-print" rows="8" placeholder="Enter player names here..."></textarea> | |
</div> | |
<div class="d-grid"> | |
<button type="button" class="btn btn-primary no-print" onclick="generatePlan()">Generate Play Plan</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
<div class="mt-4" id="output"></div> | |
</div> | |
<div class="container mt-3 no-print"> | |
<div class="d-grid"> | |
<button type="button" class="btn btn-success" onclick="window.print()">Print Play Plan</button> | |
</div> | |
</div> | |
<!-- Bootstrap 5 JS and Popper.js --> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script> | |
// Constants | |
const MIN_PLAYERS = 3; | |
const MIN_GROUP_SIZE = 3; | |
const MAX_GROUP_SIZE = 4; | |
const ROUNDS = 3; | |
// Track player matchups | |
class TournamentManager { | |
constructor(players) { | |
this.players = players; | |
this.matchHistory = new Map(); // Track who played against whom | |
this.rounds = []; | |
} | |
initializeMatchHistory() { | |
this.players.forEach(player => { | |
this.matchHistory.set(player, new Set()); | |
}); | |
} | |
recordMatchups(group) { | |
for (let i = 0; i < group.length; i++) { | |
for (let j = i + 1; j < group.length; j++) { | |
if (!this.matchHistory.has(group[i])) { | |
this.matchHistory.set(group[i], new Set()); | |
} | |
if (!this.matchHistory.has(group[j])) { | |
this.matchHistory.set(group[j], new Set()); | |
} | |
this.matchHistory.get(group[i]).add(group[j]); | |
this.matchHistory.get(group[j]).add(group[i]); | |
} | |
} | |
} | |
havePlayed(player1, player2) { | |
return this.matchHistory.get(player1)?.has(player2) || false; | |
} | |
// Calculate how many new opponents a player would have in a group | |
calculateNewOpponents(player, group) { | |
return group.filter(member => !this.havePlayed(player, member)).length; | |
} | |
findBestGroup(player, groups) { | |
let bestGroup = null; | |
let maxNewOpponents = -1; | |
for (let group of groups) { | |
if (group.length >= MAX_GROUP_SIZE) continue; | |
const newOpponents = this.calculateNewOpponents(player, group); | |
if (newOpponents > maxNewOpponents) { | |
maxNewOpponents = newOpponents; | |
bestGroup = group; | |
} | |
} | |
return bestGroup; | |
} | |
generateRound(shuffledPlayers) { | |
const groups = [[]]; | |
const unplacedPlayers = []; | |
// First pass - try to place players with optimal matchups | |
for (const player of shuffledPlayers) { | |
const bestGroup = this.findBestGroup(player, groups); | |
if (bestGroup && this.calculateNewOpponents(player, bestGroup) > 0) { | |
bestGroup.push(player); | |
} else { | |
unplacedPlayers.push(player); | |
} | |
} | |
// Second pass - handle unplaced players | |
for (const player of unplacedPlayers) { | |
let placed = false; | |
// Try to find any group that's not full | |
for (const group of groups) { | |
if (group.length < MAX_GROUP_SIZE) { | |
group.push(player); | |
placed = true; | |
break; | |
} | |
} | |
// If no existing group works, create a new one | |
if (!placed) { | |
groups.push([player]); | |
} | |
} | |
// Remove empty groups and balance if needed | |
const finalGroups = groups.filter(group => group.length > 0); | |
this.balanceGroups(finalGroups); | |
// Record all matchups in this round | |
finalGroups.forEach(group => this.recordMatchups(group)); | |
return finalGroups; | |
} | |
balanceGroups(groups) { | |
let iterations = 0; | |
const maxIterations = 100; // Prevent infinite loops | |
while (groups.some(group => group.length < MIN_GROUP_SIZE) && iterations < maxIterations) { | |
const smallGroup = groups.find(group => group.length < MIN_GROUP_SIZE); | |
const largeGroup = groups.find(group => group.length > MIN_GROUP_SIZE); | |
if (largeGroup) { | |
// Find the best player to move (least conflicts) | |
let bestPlayer = null; | |
let leastConflicts = Infinity; | |
for (const player of largeGroup) { | |
const conflicts = smallGroup.filter(member => | |
this.havePlayed(player, member) | |
).length; | |
if (conflicts < leastConflicts) { | |
leastConflicts = conflicts; | |
bestPlayer = player; | |
} | |
} | |
if (bestPlayer) { | |
largeGroup.splice(largeGroup.indexOf(bestPlayer), 1); | |
smallGroup.push(bestPlayer); | |
} | |
} else { | |
// If we can't balance properly, merge small groups | |
const groupToMerge = groups.find(g => g !== smallGroup); | |
if (groupToMerge) { | |
smallGroup.push(...groupToMerge); | |
groups.splice(groups.indexOf(groupToMerge), 1); | |
} | |
} | |
iterations++; | |
} | |
} | |
generateTournament() { | |
this.initializeMatchHistory(); | |
for (let round = 0; round < ROUNDS; round++) { | |
const shuffledPlayers = shuffleArray([...this.players]); | |
const roundGroups = this.generateRound(shuffledPlayers); | |
this.rounds.push(roundGroups); | |
} | |
return this.rounds; | |
} | |
} | |
// Utility functions | |
const getPlayers = () => { | |
const playerInput = document.getElementById("playerNames").value.trim(); | |
return playerInput.split("\n") | |
.map(name => name.trim()) | |
.filter(name => name); | |
}; | |
const shuffleArray = (array) => { | |
for (let i = array.length - 1; i > 0; i--) { | |
const j = Math.floor(Math.random() * (i + 1)); | |
[array[i], array[j]] = [array[j], array[i]]; | |
} | |
return array; | |
}; | |
const generatePlan = () => { | |
const players = getPlayers(); | |
if (players.length < MIN_PLAYERS) { | |
alert(`Please enter at least ${MIN_PLAYERS} player names.`); | |
return; | |
} | |
const tournament = new TournamentManager(players); | |
const rounds = tournament.generateTournament(); | |
displayTournament(rounds); | |
}; | |
const displayTournament = (rounds) => { | |
const outputDiv = document.getElementById("output"); | |
outputDiv.innerHTML = rounds | |
.map((round, roundIndex) => ` | |
<div class="round-container mb-5"> | |
<h3 class="text-center">Round ${roundIndex + 1}</h3> | |
${round.map((group, groupIndex) => ` | |
<div class="group-container mb-4"> | |
<h5>Group ${groupIndex + 1}: ${group.join(", ")}</h5> | |
<table class="table table-bordered"> | |
<thead class="table-light"> | |
<tr> | |
<th>Name</th> | |
<th style="width: 30%;">Well-Architected Points</th> | |
</tr> | |
</thead> | |
<tbody> | |
${group.map(player => ` | |
<tr> | |
<td>${player}</td> | |
<td></td> | |
</tr> | |
`).join("")} | |
</tbody> | |
</table> | |
</div> | |
`).join("")} | |
</div> | |
`).join(""); | |
}; | |
// Event listeners | |
document.addEventListener("DOMContentLoaded", () => { | |
const generateButton = document.querySelector("button[onclick='generatePlan()']"); | |
if (generateButton) { | |
generateButton.onclick = generatePlan; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment