X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=js%2Findex.js;h=b51d590dc128dcfff78b4404cec2f19862c4bd38;hb=942ca610ac679dba7ed09c10829d38b29c09bccc;hp=42a376efedf07f383b047fbb4c0fabc80fb3f9d9;hpb=48b3a536373c1ee477fdff3e1f30bc3515d9e568;p=westcastle.git diff --git a/js/index.js b/js/index.js index 42a376e..b51d590 100644 --- a/js/index.js +++ b/js/index.js @@ -6,7 +6,7 @@ new Vue({ }, components: { 'my-players': { - props: ['players'], + props: ['players','initPlayers'], template: `
@@ -27,6 +27,12 @@ new Vue({
+
+ + +
`, computed: { @@ -41,12 +47,22 @@ new Vue({ 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: [], @@ -59,7 +75,14 @@ new Vue({ template: `
- +
+ + +

Table {{ index+1 }}

@@ -83,24 +106,111 @@ new Vue({ {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }} - +
- - -
-
- Attention: un score a déjà été enregistré. - Les points indiqués ici s'ajouteront : il faut d'abord - restaurer l'état précédent. - Si c'est déjà fait, ignorer ce message :) + + +
`, methods: { + // TODO: télécharger la ronde courante + // TODO: mémoriser les appariements passés pour éviter que les mêmes joueurs se rencontrent plusieurs fois + // --> dans la base: tableau rounds, rounds[0] : {tables[0,1,...], chacune contenant 4 indices de joueurs; + sessions[0,1,...]} + // --> devrait séparer les components en plusieurs fichiers... + // cas à 5 joueurs : le joueur exempt doit tourner (c'est fait automatiquement en fait) + cancelRound: function() { + this.scored.forEach( (s,i) => { + if (s) + { + // Cancel this table + this.currentIndex = i; //TODO: clumsy. funcions should take "index" as argument + this.resetScore(); + } + }); + this.currentIndex = -1; + this.doPairings(); + }, doPairings: function() { + let rounds = JSON.parse(localStorage.getItem("rounds")); + + if (this.scored.some( s => { return s; })) + { + this.commitScores(); //TODO: temporary: shouldn't be here... (incremental commit) + if (rounds === null) + rounds = []; + rounds.push(this.tables); + } + + // 1) Compute the "meeting" matrix: who played who and how many times + let meetMat = _.range(this.players.length).map( i => { + _.range(this.players.length).map( j => { + return 0; + }); + }); + rounds.forEach( r => { //for each round + r.forEach( t => { //for each table within round + for (let i=0; i<4; i++) //TODO: these loops are ugly + { + for (let j=0; j<4; j++) + { + if (j!=i) + meetMat[i][j]++; + } + } + }); + }); + + // 2) Pre-compute tables repartition (in numbers): depends on active players count % 4 + let activePlayers = this.players + .map( (p,i) => { return Object.Assign({}, p, {index:i}); }) + .filter( p => { return p.available; }); + let repartition = _.times(Math.floor(activePlayers.length/4), _.constant(4)); + switch (activePlayers.length % 4) + { + case 1: + // Need 2 more + if (repartition.length-1 >= 2) + { + repartition[0]--; + repartition[1]--; + repartition[repartition.length-1] += 2; + } + break; + case 2: + // Need 1 more + if (repartition.length-1 >= 1) + { + repartition[0]--; + repartition[repartition.length-1]++; + } + break; + } + + // 3) Sort people by total games played (increasing) - naturally solve the potential unpaired case + let totalGames = _.range(this.players.length).map( i => { return 0; }); + rounds.forEach( r => { + r.forEach(t => { + t.forEach( p => { + totalGames[p]++; + }) + }) + }); + let sortedPlayers = activePlayers + .map( (p,i) => { return Object.Assign({}, p, {games:totalGames[p.index]}); }) + .sort( (a,b) => { return a.games - b.games; }); + + // 4) Affect people on tables, following total games sorted order (with random sampling on ex-aequos) + // --> et surtout en minimisant la somme des rencontres précédentes (ci-dessus : cas particulier rare à peu de joueurs) +//TODO // Simple case first: 4 by 4 let tables = []; let currentTable = []; @@ -152,33 +262,57 @@ new Vue({ 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:s, index:i}; }) - .sort( (a,b) => { return parseInt(b.value) - parseInt(a.value); }); - let pdts = [4, 2, 1, 0]; + .map( (s,i) => { return {value:parseInt(s), index:i}; }) + .sort( (a,b) => { return b.value - a.value; }); + 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(); + }, + 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); }, }, }, @@ -187,13 +321,17 @@ new Vue({ return { time: 0, //remaining time, in seconds running: false, + initialTime: 90, //1h30, in minutes + setter: false, + setterTime: 0, //to input new initial time }; }, template: ` -
-
+
+
{{ formattedTime }}
+
`, @@ -203,8 +341,19 @@ new Vue({ let minutes = Math.floor(this.time / 60); return this.padToZero(minutes) + ":" + this.padToZero(seconds); }, + divHeight: function() { + return screen.height; + }, + divWidth: function() { + return screen.width; + }, }, methods: { + setTime: function() { + this.initialTime = this.setterTime; + this.setter = false; + this.reset(); + }, padToZero: function(a) { if (a < 10) return "0" + a; @@ -217,7 +366,7 @@ new Vue({ }, reset: function(e) { this.running = false; - this.time = 5400; //1:30 + this.time = this.initialTime * 60; }, start: function() { if (!this.running) @@ -228,6 +377,8 @@ new Vue({ this.running = false; return; } + if (this.time == this.initialTime) + new Audio("sounds/gong.mp3").play(); //gong at the beginning setTimeout(() => { if (this.running) this.time--; @@ -236,11 +387,30 @@ new Vue({ }, }, created: function() { + this.setterTime = this.initialTime; this.reset(); }, + mounted: function() { + let timer = document.getElementById("timer"); + let keyDict = { + 32: () => { this.setter = true; }, //Space + 27: () => { this.setter = false; }, //Esc + }; + document.addEventListener("keyup", e => { + if (timer.style.display !== "none") + { + let func = keyDict[e.keyCode]; + if (!!func) + { + e.preventDefault(); + func(); + } + } + }); + }, }, 'my-ranking': { - props: ['players','sortByScore','writeScoreToDb'], + props: ['players','sortByScore','commitScores'], template: `
@@ -258,8 +428,11 @@ new Vue({
- - + + +
`, @@ -282,85 +455,86 @@ new Vue({ rankPeople: function() { return this.players .slice(1) //discard Toto - .map( p => { return Object.assign({}, p); }) //to not alter original array .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(); //TODO: hack... - }, - 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, - }); - self.players = players; - } - } - }; - 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 - .map( p => { return Object.assign({}, p); }) //deep (enough) copy - .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(","); + let p = { prenom: parts[0], nom: parts[1] }; + p.pdt = parts.length > 2 ? parseFloat(parts[2]) : 0; + p.session = parts.length > 3 ? parseInt(parts[3]) : 0; + p.available = parts.length > 4 ? parts[4] : 1; + return p; + }); + this.addToto(players); + this.players = players; + this.commitScores(); //save players in memory }, }, });