Skip to content

Commit 38f68e7

Browse files
Merge pull request #4 from isaacbatst/feat/elo-system
Feat/elo system
2 parents 3747916 + 5ffd6dc commit 38f68e7

File tree

4 files changed

+12019
-27
lines changed

4 files changed

+12019
-27
lines changed

elo.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Rating, quality, rate } from './lib/true-skill/bundle.js';
2+
3+
const createInitialRating = () => {
4+
const r = new Rating()
5+
return {
6+
mu: r.mu,
7+
sigma: r.sigma
8+
}
9+
}
10+
11+
function calculateMatchQuality(teamA, teamB) {
12+
// Extract players' ratings (mu, sigma) from each team
13+
const ratingsA = teamA.map(player => new Rating(player.mu, player.sigma));
14+
const ratingsB = teamB.map(player => new Rating(player.mu, player.sigma));
15+
16+
// Calculate the match quality
17+
return quality([ratingsA, ratingsB]);
18+
}
19+
20+
function getAllTeamCombinations(players) {
21+
const halfSize = Math.floor(players.length / 2);
22+
23+
function* combinations(arr, k, start = 0) {
24+
if (k === 0) {
25+
yield [];
26+
return;
27+
}
28+
for (let i = start; i <= arr.length - k; i++) {
29+
for (const combo of combinations(arr, k - 1, i + 1)) {
30+
yield [arr[i], ...combo];
31+
}
32+
}
33+
}
34+
35+
const teamPairs = [];
36+
const seenCombinations = new Set(); // Armazena as combinações vistas
37+
38+
for (const teamA of combinations(players, halfSize)) {
39+
const teamB = players.filter(p => !teamA.includes(p));
40+
41+
// Cria uma chave única ordenando os dois times internamente e comparando-os em conjunto
42+
const sortedTeams = [teamA.map(p => p.name).sort(), teamB.map(p => p.name).sort()].sort();
43+
const key = JSON.stringify(sortedTeams);
44+
45+
if (!seenCombinations.has(key)) {
46+
teamPairs.push([teamA, teamB]);
47+
seenCombinations.add(key); // Armazena a combinação única
48+
}
49+
}
50+
51+
return teamPairs;
52+
}
53+
54+
function findBestTeamMatch(players) {
55+
const teamCombinations = getAllTeamCombinations(players);
56+
const combinationsWithQuality = teamCombinations.map(([teamA, teamB]) => {
57+
const matchQuality = calculateMatchQuality(teamA, teamB);
58+
return { teamA, teamB, matchQuality };
59+
})
60+
61+
const sortedCombinations = combinationsWithQuality.sort((a, b) => b.matchQuality - a.matchQuality);
62+
return sortedCombinations[0];
63+
}
64+
65+
const updateRatings = (victoryTeam, defeatTeam) => {
66+
const victoryTeamRatings = victoryTeam.map(player => new Rating(player.mu, player.sigma));
67+
const defeatTeamRatings = defeatTeam.map(player => new Rating(player.mu, player.sigma));
68+
const [
69+
victoryTeamRatingsUpdated,
70+
defeatTeamRatingsUpdated
71+
] = rate([victoryTeamRatings, defeatTeamRatings]);
72+
73+
return [
74+
victoryTeam.map((player, i) => ({
75+
...player,
76+
mu: victoryTeamRatingsUpdated[i].mu,
77+
sigma: victoryTeamRatingsUpdated[i].sigma
78+
})),
79+
defeatTeam.map((player, i) => ({
80+
...player,
81+
mu: defeatTeamRatingsUpdated[i].mu,
82+
sigma: defeatTeamRatingsUpdated[i].sigma
83+
}))
84+
]
85+
}
86+
87+
88+
export { createInitialRating, findBestTeamMatch, updateRatings };

index.html

+10-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<button id="show-historic" class="button is-primary is-large"><i class="fa-regular fa-clock"></i></i>&ensp;Histórico</button>
1616
</div>
1717
<div class="container mobile" style="padding: 15px">
18-
18+
1919
<div id="new-match-day-form" style="display: none">
2020
<div class="columns">
2121
<div class="column">
@@ -103,6 +103,7 @@ <h1 id="team-2-captain" class="title">TEAM 2 CAPTAIN</h1>
103103
<th>Vitórias</th>
104104
<th>Derrotas</th>
105105
<th>Última partida jogada</th>
106+
<th style="display: none;" id="elo-header">Elo</th>
106107
<th id="playing">Jogando</th>
107108
</tr>
108109
</thead>
@@ -111,7 +112,13 @@ <h1 id="team-2-captain" class="title">TEAM 2 CAPTAIN</h1>
111112
</div>
112113
</nav>
113114

114-
<button id="end-match-day" class="button is-danger is-large" style="display: none;">FINALIZAR PELADA</button>
115+
<div class="is-flex is-justify-content-space-between is-align-items-center is-flex-wrap-wrap" style="gap: 1rem">
116+
<button id="end-match-day" class="button is-danger is-large" style="display: none;">FINALIZAR PELADA</button>
117+
<label class="checkbox">
118+
<input type="checkbox" id="dev-mode" />
119+
Modo desenvolvedor
120+
</label>
121+
</div>
115122
</div>
116123
<div class="container mobile" id="history-container" style="padding: 15px; display: none;">
117124
<h1>Histórico de Partidas</h1>
@@ -124,7 +131,7 @@ <h1>Histórico de Partidas</h1>
124131
src="https://code.jquery.com/jquery-3.7.1.min.js"
125132
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
126133
crossorigin="anonymous"></script>
127-
<script src="./index.js?v=9"></script>
134+
<script type="module" src="./index.js?v=9"></script>
128135
<script>
129136
if('serviceWorker' in navigator) {
130137
navigator.serviceWorker.register('./serviceWorker.js', { scope: '/peladaManager/' });

index.js

+106-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { createInitialRating, findBestTeamMatch, updateRatings } from './elo.js';
2+
13
const LOCAL_STORAGE_KEY = "matche_days";
4+
const LOCAL_STORAGE_ELO_KEY = "players_elo";
5+
26
let maxPoints;
37
let currentMatchMaxPoints;
48
let playersPerTeam;
@@ -52,6 +56,33 @@ function getFromLocalStorage() {
5256
return gameDays || [];
5357
}
5458

59+
function getRatingsFromStorage(players) {
60+
const playersElo = JSON.parse(localStorage.getItem(LOCAL_STORAGE_ELO_KEY));
61+
return players.map(player => {
62+
return {
63+
...player,
64+
...playersElo[player.name]
65+
}
66+
});
67+
}
68+
69+
function storeUpdatedRatings([updatedVictory, updatedLosing]) {
70+
const playersElo = JSON.parse(localStorage.getItem(LOCAL_STORAGE_ELO_KEY));
71+
updatedVictory.forEach(player => {
72+
playersElo[player.name] = {
73+
mu: player.mu,
74+
sigma: player.sigma
75+
}
76+
});
77+
updatedLosing.forEach(player => {
78+
playersElo[player.name] = {
79+
mu: player.mu,
80+
sigma: player.sigma
81+
}
82+
});
83+
localStorage.setItem(LOCAL_STORAGE_ELO_KEY, JSON.stringify(playersElo));
84+
}
85+
5586
function sortPlayers(a, b) {
5687
if (a.lastPlayedMatch === b.lastPlayedMatch) {
5788
if (a.matches < b.matches) return -1;
@@ -65,18 +96,34 @@ function sortPlayers(a, b) {
6596
return 0;
6697
}
6798

99+
$('#dev-mode').change(function() {
100+
updatePlayerList();
101+
})
102+
68103
function updatePlayerList() {
69104
$("#players").empty();
70-
const playersToList = players.sort((a, b) => sortPlayers(a, b));
105+
const playersToList = getRatingsFromStorage(players)
106+
.sort((a, b) => sortPlayers(a, b))
107+
108+
const devMode = $("#dev-mode").is(":checked");
109+
if(devMode) {
110+
$("#elo-header").show();
111+
} else {
112+
$("#elo-header").hide();
113+
}
114+
71115
playersToList.forEach(player => {
72116
const playerIsPlayingNow = playingTeams.flat().some(p => p.name === player.name);
117+
const formatter = new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2, minimumFractionDigits: 2 });
118+
const elo = devMode ? `${formatter.format(player.mu)}/${formatter.format(player.sigma)}` : '';
73119
$("#players").append(`
74120
<tr ${playerIsPlayingNow ? 'class="is-selected"' : ''}>
75121
<th>${player.name}</th>
76122
<td>${player.matches}</td>
77123
<td>${player.victories}</td>
78124
<td>${player.defeats}</td>
79125
<td>${player.lastPlayedMatch}</td>
126+
${devMode ? `<td>${elo}</td>` : ''}
80127
<td ${!player.playing ? 'class="is-danger remove-player"' : 'class="remove-player"'} style="cursor: pointer">${player.playing ? 'Sim ' : 'Não'} ${playerIsPlayingNow ? '<i class="fa-solid fa-repeat"></i>' : '<i class="fa-solid fa-volleyball"></i>'}</td>
81128
</tr>`);
82129
});
@@ -137,6 +184,13 @@ $("#new-match-day").click(function() {
137184
$("#new-match-day-button").hide();
138185
});
139186

187+
function addPlayerToEloSystem(player) {
188+
const playersElo = JSON.parse(localStorage.getItem(LOCAL_STORAGE_ELO_KEY));
189+
if(playersElo[player.name]) return;
190+
playersElo[player.name] = createInitialRating();
191+
localStorage.setItem(LOCAL_STORAGE_ELO_KEY, JSON.stringify(playersElo));
192+
}
193+
140194
function addNewPlayer(){
141195
const playerName = $("#new-player-name").val();
142196

@@ -161,6 +215,7 @@ function addNewPlayer(){
161215
lastPlayedMatch: 0,
162216
playing: true,
163217
});
218+
addPlayerToEloSystem({ name: playerName });
164219

165220
$("#player-list").append(`<li>${playerName}</li>`);
166221

@@ -175,21 +230,13 @@ $("input").on("keydown",function search(e) {
175230
}
176231
});
177232

178-
function generateRandomTeams(players) {
179-
const teams = [];
180-
// Generate two random teams
181-
for (let i = 0; i < 2; i++) {
182-
const team = [];
183-
for (let j = 0; j < playersPerTeam; j++) {
184-
const playerIndex = Math.floor(Math.random() * players.length);
185-
const player = players[playerIndex];
186-
team.push(player);
187-
players.splice(playerIndex, 1);
188-
}
189-
teams.push(team);
190-
}
191-
192-
return teams;
233+
function generateTeams(players) {
234+
const playersWithElo = getRatingsFromStorage(players);
235+
const bestMatch = findBestTeamMatch(playersWithElo);
236+
return [
237+
bestMatch.teamA,
238+
bestMatch.teamB
239+
];
193240
}
194241

195242
function updateCurrentMatch(teams) {
@@ -249,7 +296,7 @@ $("#start-match-day").click(function() {
249296

250297

251298
saveOnLocalStorage();
252-
updateCurrentMatch(generateRandomTeams(firstPlayers));
299+
updateCurrentMatch(generateTeams(firstPlayers));
253300
randomServe();
254301
});
255302

@@ -277,6 +324,12 @@ function endMatch(victoryTeam) {
277324
alert(`Time ${playingTeams[victoryTeam][0].name} venceu a partida!`);
278325
matches += 1;
279326

327+
const victoryTeamRating = getRatingsFromStorage(playingTeams[victoryTeam])
328+
const losingTeamRating = getRatingsFromStorage(playingTeams[1 - victoryTeam])
329+
330+
const updatedRatings = updateRatings(victoryTeamRating, losingTeamRating);
331+
storeUpdatedRatings(updatedRatings);
332+
280333
// Add one match to every player and one victory to each player on winning team
281334
playingTeams[victoryTeam].forEach(player => {
282335
const playerIndex = players.findIndex(p => p.name === player.name);
@@ -298,15 +351,24 @@ function endMatch(victoryTeam) {
298351
saveOnLocalStorage();
299352
}
300353

354+
function findPlayerByName(players, name) {
355+
return players.find(player => player.name === name);
356+
}
357+
301358
function startNewMatch(winningPlayers, losingPlayers) {
302359
$("#match").show();
303360
$("#score-team-1").text("0");
304361
$("#score-team-2").text("0");
305362

306-
const notPlayingPlayers = players.filter(player => !player.playing);
363+
const notPlayingPlayers = players
364+
.filter(player => !player.playing);
307365
let newPlayers = [...winningPlayers];
308366
let playersToPlay = [];
309-
let playerList = players.sort((a, b) => sortPlayers(a, b)).filter(player => !winningPlayers.includes(player) && !losingPlayers.includes(player) && !notPlayingPlayers.includes(player));
367+
let playerList = players
368+
.sort((a, b) => sortPlayers(a, b))
369+
.filter(player => !findPlayerByName(winningPlayers, player.name)
370+
&& !findPlayerByName(losingPlayers, player.name)
371+
&& !findPlayerByName(notPlayingPlayers, player.name));
310372

311373
// Is there any players that didn't play yet?
312374
if (players.length > playersPerTeam * 2) {
@@ -320,19 +382,25 @@ function startNewMatch(winningPlayers, losingPlayers) {
320382
for (let i = 0; i < playersPerTeam; i++) {
321383
const playerIndex = Math.floor(Math.random() * playersToPlay.length);
322384
const player = playersToPlay[playerIndex];
323-
if (!newPlayers.includes(player)) {
385+
if (!findPlayerByName(newPlayers, player.name)) {
324386
newPlayers.push(player);
325387
}
326388
}
327389
}
328390
} else {
329391
// I don't have substitutes to play, let's just keep playing
330-
updateCurrentMatch(generateRandomTeams(players));
392+
updateCurrentMatch(generateTeams(players));
331393
return;
332394
}
333395

334396
// Remove players that are already playing, get players with less matches but that played the longest time ago
335-
const sortedPlayers = players.filter(player => !newPlayers.includes(player) && !winningPlayers.includes(player) && !playerList.includes(player) && !notPlayingPlayers.includes(player)).sort((a, b) => sortPlayers(a, b)).slice(0, (playersPerTeam * 2) - newPlayers.length);
397+
const sortedPlayers = players
398+
.filter(player => !findPlayerByName(newPlayers, player.name)
399+
&& !findPlayerByName(winningPlayers, player.name)
400+
&& !findPlayerByName(playerList, player.name)
401+
&& !findPlayerByName(notPlayingPlayers, player.name))
402+
.sort((a, b) => sortPlayers(a, b))
403+
.slice(0, (playersPerTeam * 2) - newPlayers.length);
336404

337405
while (newPlayers.length < playersPerTeam * 2) {
338406
// Just to be sure, remove any duplicates
@@ -341,14 +409,14 @@ function startNewMatch(winningPlayers, losingPlayers) {
341409
const playerIndex = Math.floor(Math.random() * sortedPlayers.length);
342410
const player = sortedPlayers[playerIndex];
343411

344-
if (!newPlayers.includes(player)) {
412+
if (!findPlayerByName(newPlayers, player.name)) {
345413
newPlayers.push(player);
346414
}
347415
}
348416

349417
currentMatchMaxPoints = maxPoints;
350418
randomServe();
351-
updateCurrentMatch(generateRandomTeams(newPlayers));
419+
updateCurrentMatch(generateTeams(newPlayers));
352420
saveOnLocalStorage();
353421
}
354422

@@ -574,8 +642,22 @@ $("#historic-days").on("click", ".match-historic", function() {
574642
showFinalPlayerList(playersByWinPercentage);
575643
});
576644

645+
function initEloSystem(players) {
646+
const playersElo = {}
647+
players.forEach(player => {
648+
const rating = createInitialRating();
649+
playersElo[player.name] = rating;
650+
})
651+
localStorage.setItem(LOCAL_STORAGE_ELO_KEY, JSON.stringify(playersElo));
652+
}
653+
577654
$(document).ready(function (){
578655
const gameDays = getFromLocalStorage();
656+
const hasElo = localStorage.getItem(LOCAL_STORAGE_ELO_KEY);
657+
658+
if(!hasElo) {
659+
initEloSystem(gameDays.flatMap(gameDay => gameDay.players));
660+
}
579661

580662
if (gameDays.length > 0) {
581663
const lastGameDay = gameDays[gameDays.length - 1];

0 commit comments

Comments
 (0)