</head>
<body><span class="tldr">TL;DR:</span>
<ol>
- <li>Cliquer sur les joueurs absents dans l'onglet "joueurs"</li>
- <li>Aller dans la section "appariements" et cliquer sur le bouton en haut</li>
- <li>Lancer le chrono (section "chronomètre", puis click gauche [puis F11])</li>
- <li>À la fin d'une ronde, cliquer sur chaque table pour indiquer les (mini-)points.</li>
+ <li>Cliquer sur les joueurs absents dans l'onglet "Participants"</li>
+ <li>Aller dans la section "appariements" et cliquer sur le bouton "Nouvelle ronde"</li>
+ <li>Lancer le chrono (section "Chronomètre", puis click gauche [puis F11])</li>
+ <li>Après la ronde, cliquer sur chaque table pour indiquer les (mini-)points, <strong>puis cliquer sur "Valider".</strong></li>
</ol>
- <p>Le classement est mis à jour dans la rubrique correspondante et dans joueurs.csv.</p>
+ <p>Le classement est mis à jour dans la rubrique correspondante.</p>
<p>Voir aussi la <a href="https://auder.net/westcastle_demo.webm">vidéo de démonstration</a></p>
<hr><span>L'application est divisée en 4 sections :</span>
<ul>
<li>Participants : la liste des joueurs présents (et absents)</li>
<li>Appariements : répartition des joueurs présents par tables</li>
<li>Chronomètre : un chrono qui démarre à 1h30 et sonne une fois à zéro</li>
- <li>Classement : le tableau des scores (voir aussi joueurs.csv)</li>
+ <li>Classement : le tableau des scores</li>
</ul>
<h2>Participants</h2>
<p>
et la table s'affiche désormais sur fond vert. Un clic sur "Fermer" annule l'opération, aucun changement n'est effectué.
</p>
- <p>En cas d'erreur, <strong>ne surtout pas scorer une nouvelle table :</strong> d'abord restaurer l'état précédent en cliquant sur le bouton "Restaurer"
- sur l'écran de classement ; alternativement, on peut cliquer sur le lien présent dans l'avertissement lorsqu'on reclique sur la table en question.
- Ensuite, rentrer les scores corrigés.
+ <p>
+ En cas d'erreur, re-cliquer sur la table à corriger : cliquer ensuite sur "Annuler", puis ré-entrer les scores.
</p>
<h2>Chronomètre</h2>
<h2>Classement</h2>
<p>
Les scores sont ordonnés par points de table décroissants d'abord, puis en cas d'ex-aequos par mini-points décroissants.
+ Ils peuvent être téléchargés via le bouton en haut à droite. Dans le fichier, "pdt" indique les points de table cumulés
+ et "session" les points de session cumulés (aussi appelés mini-points).
</p>
- <p>
- Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois.
- Cette action peut être annulée (immédiatement après) en cliquant sur "Restaurer".
- </p>
+ <p>Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois.<strong>Attention cette action est irréversible.</strong></p>
</body>
</html>
\ No newline at end of file
span.tldr TL;DR:
ol
- li Cliquer sur les joueurs absents dans l'onglet "joueurs"
- li Aller dans la section "appariements" et cliquer sur le bouton en haut
- li Lancer le chrono (section "chronomètre", puis click gauche [puis F11])
- li À la fin d'une ronde, cliquer sur chaque table pour indiquer les (mini-)points.
- p Le classement est mis à jour dans la rubrique correspondante et dans joueurs.csv.
+ li Cliquer sur les joueurs absents dans l'onglet "Participants"
+ li Aller dans la section "appariements" et cliquer sur le bouton "Nouvelle ronde"
+ li Lancer le chrono (section "Chronomètre", puis click gauche [puis F11])
+ li Après la ronde, cliquer sur chaque table pour indiquer les (mini-)points, #[strong puis cliquer sur "Valider".]
+ p Le classement est mis à jour dans la rubrique correspondante.
p Voir aussi la #[a(href="https://auder.net/westcastle_demo.webm") vidéo de démonstration]
li Participants : la liste des joueurs présents (et absents)
li Appariements : répartition des joueurs présents par tables
li Chronomètre : un chrono qui démarre à 1h30 et sonne une fois à zéro
- li Classement : le tableau des scores (voir aussi joueurs.csv)
+ li Classement : le tableau des scores
h2 Participants
et la table s'affiche désormais sur fond vert. Un clic sur "Fermer" annule l'opération, aucun changement n'est effectué.
p.
- En cas d'erreur, #[strong ne surtout pas scorer une nouvelle table :] d'abord restaurer l'état précédent en cliquant sur le bouton "Restaurer"
- sur l'écran de classement ; alternativement, on peut cliquer sur le lien présent dans l'avertissement lorsqu'on reclique sur la table en question.
- Ensuite, rentrer les scores corrigés.
+ En cas d'erreur, re-cliquer sur la table à corriger : cliquer ensuite sur "Annuler", puis ré-entrer les scores.
h2 Chronomètre
p.
Les scores sont ordonnés par points de table décroissants d'abord, puis en cas d'ex-aequos par mini-points décroissants.
+ Ils peuvent être téléchargés via le bouton en haut à droite. Dans le fichier, "pdt" indique les points de table cumulés
+ et "session" les points de session cumulés (aussi appelés mini-points).
- p.
- Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois.
- Cette action peut être annulée (immédiatement après) en cliquant sur "Restaurer".
+ p
+ | Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois.
+ strong Attention cette action est irréversible.
},
components: {
'my-players': {
- props: ['players'],
+ props: ['players','initPlayers'],
template: `
<div id="players">
<div class="left">
</tr>
</table>
</div>
+ <div class="clear">
+ <input class="hide" id="upload" type="file" @change="upload"/>
+ <button class="btn block cancel" @click="uploadTrigger()" title="Charge la liste des joueurs, en principe en début de tournoi">
+ (Ré)initialiser
+ </button>
+ </div>
</div>
`,
computed: {
methods: {
toggleAvailability: function(i) {
this.players[i].available = 1 - this.players[i].available;
- this.$forceUpdate(); //TODO (Vue.set... ?!)
+ },
+ uploadTrigger: function() {
+ document.getElementById("upload").click();
+ },
+ upload: function(e) {
+ let file = (e.target.files || e.dataTransfer.files)[0];
+ var reader = new FileReader();
+ reader.onloadend = ev => {
+ this.initPlayers(ev.currentTarget.result);
+ };
+ reader.readAsText(file);
},
},
},
'my-pairings': {
- props: ['players','writeScoreToDb'],
+ props: ['players','commitScores'],
data: function() {
return {
unpaired: [],
template: `
<div id="pairings">
<div v-show="currentIndex < 0">
- <button id="runPairing" class="block btn" @click="doPairings()">Nouvelle ronde</button>
+ <div class="button-container-horizontal">
+ <button class="btn validate" :class="{hide: tables.length==0}" @click="commitScores()" title="Valide l'état actuel des scores (cf. rubrique Classement) en mémoire. Cette action est nécessaire au moins une fois en fin de tournoi après toutes les parties, et est recommandée après chaque ronde">
+ Valider
+ </button>
+ <button id="doPairings" class="btn" :class="{cancel: tables.length>0}" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
+ Nouvelle ronde
+ </button>
+ </div>
<div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}"
@click="showScoreForm(table,index)">
<p>Table {{ index+1 }}</p>
<td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
{{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
</td>
- <td><input type="text" v-model="sessions[currentIndex][i]"/></td>
+ <td><input type="text" v-model="sessions[currentIndex][i]" :disabled="scored[currentIndex]"/></td>
</tr>
</table>
<div class="button-container-horizontal">
- <button class="btn validate" @click="setScore()">Enregistrer</button>
- <button class="btn" @click="currentIndex = -1">Fermer</button>
- </div>
- <div v-if="scored[currentIndex]" class="warning">
- Attention: un score a déjà été enregistré.
- Les points indiqués ici s'ajouteront : il faut d'abord
- <span class="link" @click="clickRestore()">restaurer l'état précédent.</span>
- Si c'est déjà fait, ignorer ce message :)
+ <button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base (la rubrique Classement est mise à jour)">
+ Enregistrer
+ </button>
+ <button :class="{hide:!scored[currentIndex]}" class="btn cancel" @click="resetScore()" title="Annule le score précédemment enregistré : l'action est visible dans la rubrique Classement">
+ Annuler
+ </button>
+ <button class="btn" @click="closeScoreForm()">Fermer</button>
</div>
</div>
</div>
this.sessions[index] = _.times(table.length, _.constant(0));
this.currentIndex = index;
},
- setScore: function() {
+ closeScoreForm: function() {
+ if (!this.scored[this.currentIndex])
+ this.sessions[this.currentIndex] = [];
+ this.currentIndex = -1;
+ },
+ getPdts: function() {
let sortedSessions = this.sessions[this.currentIndex]
.map( (s,i) => { return {value:parseInt(s), index:i}; })
.sort( (a,b) => { return b.value - a.value; });
- let pdts = [4, 2, 1, 0];
+ const ref_pdts = [4, 2, 1, 0];
// NOTE: take care of ex-aequos (spread points subtotal)
let curSum = 0, curCount = 0, start = 0;
+ let sortedPdts = [];
for (let i=0; i<4; i++)
{
- // Update pdts:
- curSum += pdts[i];
+ curSum += ref_pdts[i];
curCount++;
if (i==3 || sortedSessions[i].value > sortedSessions[i+1].value)
{
let pdt = curSum / curCount;
for (let j=start; j<=i; j++)
- this.players[this.tables[this.currentIndex][sortedSessions[j].index]].pdt += pdt;
+ sortedPdts.push(pdt);
curSum = 0;
curCount = 0;
start = i+1;
}
- // Update sessions:
+ }
+ // Re-order pdts to match table order
+ let pdts = [0, 0, 0, 0];
+ for (let i=0; i<4; i++)
+ pdts[sortedSessions[i].index] = sortedPdts[i];
+ return pdts;
+ },
+ setScore: function() {
+ let pdts = this.getPdts();
+ for (let i=0; i<4; i++)
+ {
+ this.players[this.tables[this.currentIndex][i]].pdt += pdts[i];
this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]);
}
- this.scored[this.currentIndex] = true;
+ Vue.set(this.scored, this.currentIndex, true);
this.currentIndex = -1;
- this.writeScoreToDb();
},
- clickRestore: function() {
- document.getElementById('restoreBtn').click();
+ resetScore: function() {
+ let pdts = this.getPdts();
+ for (let i=0; i<4; i++)
+ {
+ this.players[this.tables[this.currentIndex][i]].pdt -= pdts[i];
+ this.players[this.tables[this.currentIndex][i]].session -= parseInt(this.sessions[this.currentIndex][i]);
+ }
+ Vue.set(this.scored, this.currentIndex, false);
},
},
},
},
},
'my-ranking': {
- props: ['players','sortByScore','writeScoreToDb'],
+ props: ['players','sortByScore','commitScores'],
template: `
<div id="ranking">
<table class="ranking">
</tr>
</table>
<div class="button-container-vertical" style="width:200px">
- <button class="btn cancel" @click="resetPlayers()">Réinitialiser</button>
- <button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button>
+ <a id="download" href="#"></a>
+ <button class="btn" @click="download()" title="Télécharge le classement courant au format CSV">Télécharger</button>
+ <button class="btn cancel" @click="resetPlayers()" title="Réinitialise les scores à zéro. ATTENTION: action irréversible">
+ Réinitialiser
+ </button>
</div>
</div>
`,
.sort(this.sortByScore);
},
resetPlayers: function() {
+ if (confirm('Êtes-vous sûr ?'))
+ {
+ this.players
+ .slice(1) //discard Toto
+ .forEach( p => {
+ p.pdt = 0;
+ p.session = 0;
+ p.available = 1;
+ });
+ this.commitScores();
+ document.getElementById("doPairings").click();
+ }
+ },
+ download: function() {
+ // Prepare file content
+ let content = "prénom,nom,pdt,session\n";
this.players
.slice(1) //discard Toto
+ .sort(this.sortByScore)
.forEach( p => {
- p.pdt = 0;
- p.session = 0;
- p.available = 1;
+ content += p.prenom + "," + p.nom + "," + p.pdt + "," + p.session + "\n";
});
- this.writeScoreToDb();
- document.getElementById("runPairing").click();
- },
- restoreLast: function() {
- let xhr = new XMLHttpRequest();
- let self = this;
- xhr.onreadystatechange = function() {
- if (this.readyState == 4 && this.status == 200)
- {
- let players = JSON.parse(xhr.responseText);
- if (players.length > 0)
- {
- players.unshift({ //add ghost 4th player for 3-players tables
- prenom: "Toto",
- nom: "",
- pdt: 0,
- session: 0,
- available: 0,
- });
- // NOTE: Vue warning "do not mutate property" if direct self.players = players
- for (let i=1; i<players.length; i++)
- {
- players[i].pdt = parseFloat(players[i].pdt);
- players[i].session = parseInt(players[i].session);
- Vue.set(self.players, i, players[i]);
- }
- }
- }
- };
- xhr.open("GET", "scripts/rw_players.php?restore=1", true);
- xhr.send(null);
+ // Prepare and trigger download link
+ let downloadAnchor = document.getElementById("download");
+ downloadAnchor.setAttribute("download", "classement.csv");
+ downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
+ downloadAnchor.click();
},
},
},
},
created: function() {
- let xhr = new XMLHttpRequest();
- let self = this;
- xhr.onreadystatechange = function() {
- if (this.readyState == 4 && this.status == 200)
- {
- let players = JSON.parse(xhr.responseText);
- players.forEach( p => {
- p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0;
- p.session = !!p.session ? parseInt(p.session) : 0;
- p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
- });
- players.unshift({ //add ghost 4th player for 3-players tables
- prenom: "Toto",
- nom: "",
- pdt: 0,
- session: 0,
- available: 0,
- });
- self.players = players;
- }
- };
- xhr.open("GET", "scripts/rw_players.php", true);
- xhr.send(null);
+ let players = JSON.parse(localStorage.getItem("players"));
+ if (players !== null)
+ {
+ this.addToto(players);
+ this.players = players;
+ }
},
methods: {
+ addToto: function(array) {
+ array.unshift({ //add ghost 4th player for 3-players tables
+ prenom: "Toto",
+ nom: "",
+ pdt: 0,
+ session: 0,
+ available: 0,
+ });
+ },
// Used both in ranking and pairings:
sortByScore: function(a,b) {
return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2;
},
- writeScoreToDb: function() {
- let xhr = new XMLHttpRequest();
- xhr.open("POST", "scripts/rw_players.php");
- xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- let orderedPlayers = this.players
- .slice(1) //discard Toto
- .sort(this.sortByScore);
- xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
+ commitScores: function() {
+ localStorage.setItem(
+ "players",
+ JSON.stringify(this.players.slice(1)) //discard Toto
+ );
+ },
+ // Used in players, reinit players array
+ initPlayers: function(csv) {
+ const allLines = csv
+ .split(/\r\n|\n|\r/) //line breaks
+ .splice(1); //discard header
+ let players = allLines
+ .filter( line => { return line.length > 0; }) //remove empty lines
+ .map( line => {
+ let parts = line.split(",");
+ return {prenom: parts[0], nom: parts[1], pdt: 0, session:0, available: 1};
+ });
+ this.addToto(players);
+ this.players = players;
},
},
});