Skip to content

Instantly share code, notes, and snippets.

@witscher
Created January 10, 2025 13:53
Show Gist options
  • Save witscher/e7696f4dda4a8d340dcc4d47e5455b3f to your computer and use it in GitHub Desktop.
Save witscher/e7696f4dda4a8d340dcc4d47e5455b3f to your computer and use it in GitHub Desktop.
<!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