update TODO
[westcastle.git] / js / index.js
index 4bffa63..226aede 100644 (file)
@@ -76,10 +76,10 @@ new Vue({
                                <div id="pairings">
                                        <div v-show="currentIndex < 0">
                                                <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 class="btn cancel" :class="{hide: tables.length==0}" @click="cancelRound()" title="Annule la ronde courante : tous les scores en cours seront perdus, et un nouveau tirage effectué. ATTENTION : action irréversible">
+                                                               Annuler
                                                        </button>
-                                                       <button id="doPairings" class="btn" :class="{cancel: tables.length>0}" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
+                                                       <button id="doPairings" class="btn" :disabled="scored.some( s => { return !s; })" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
                                                                Nouvelle ronde
                                                        </button>
                                                </div>
@@ -110,10 +110,10 @@ new Vue({
                                                        </tr>
                                                </table>
                                                <div class="button-container-horizontal">
-                                                       <button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base (la rubrique Classement est mise à jour)">
+                                                       <button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base">
                                                                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">
+                                                       <button :class="{hide:!scored[currentIndex]}" class="btn cancel" @click="cancelScore()" title="Annule le score précédemment enregistré">
                                                                Annuler
                                                        </button>
                                                        <button class="btn" @click="closeScoreForm()">Fermer</button>
@@ -122,52 +122,142 @@ new Vue({
                                </div>
                        `,
                        methods: {
+                               // TODO: télécharger la ronde courante (faudrait aussi mémoriser les points...)
+                               // --> je devrais séparer les components en plusieurs fichiers maintenant
+                               cancelRound: function() {
+                                       this.scored.forEach( (s,i) => {
+                                               if (s)
+                                               {
+                                                       // Cancel this table
+                                                       this.currentIndex = i; //TODO: clumsy. functions should take "index" as argument
+                                                       this.cancelScore();
+                                               }
+                                       });
+                                       this.currentIndex = -1;
+                                       this.doPairings();
+                               },
                                doPairings: function() {
-                                       // Simple case first: 4 by 4
+                                       let rounds = JSON.parse(localStorage.getItem("rounds")) || [];
+                                       if (this.scored.some( s => { return s; }))
+                                       {
+                                               this.commitScores(); //TODO: temporary: shouldn't be here... (incremental commit)
+                                               rounds.push(this.tables);
+                                               localStorage.setItem("rounds", JSON.stringify(rounds));
+                                       }
+                                       this.currentIndex = -1; //required if reset while scoring
                                        let tables = [];
-                                       let currentTable = [];
-                                       let ordering = _.shuffle(_.range(this.players.length));
-                                       for (let i=0; i<ordering.length; i++)
+                                       // 1) 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));
+                                       let remainder = activePlayers.length % 4;
+                                       if (remainder > 0)
+                                               repartition.push(remainder);
+                                       switch (remainder)
                                        {
-                                               if ( ! this.players[ordering[i]].available )
-                                                       continue;
-                                               if (currentTable.length >= 4)
-                                               {
-                                                       tables.push(currentTable);
-                                                       currentTable = [];
-                                               }
-                                               currentTable.push(ordering[i]);
+                                               case 1:
+                                                       // Need 2 more
+                                                       if (repartition.length-1 >= 2)
+                                                       {
+                                                               repartition[repartition.length-3] --  ;
+                                                               repartition[repartition.length-2] --  ;
+                                                               repartition[repartition.length-1] += 2;
+                                                       }
+                                                       break;
+                                               case 2:
+                                                       // Need 1 more
+                                                       if (repartition.length-1 >= 1)
+                                                       {
+                                                               repartition[repartition.length-2] --  ;
+                                                               repartition[repartition.length-1] ++  ;
+                                                       }
+                                                       break;
                                        }
-                                       // Analyse remainder
-                                       this.unpaired = [];
-                                       if (currentTable.length != 0)
+                                       // 2) Shortcut for round 1: just spread at random
+                                       if (rounds.length == 0)
                                        {
-                                               if (currentTable.length < 3)
-                                               {
-                                                       let missingPlayers = 3 - currentTable.length;
-                                                       // Pick players from 'missingPlayers' random different tables, if possible
-                                                       if (tables.length >= missingPlayers)
+                                               let currentTable = [];
+                                               let ordering = _.shuffle(_.range(activePlayers.length));
+                                               let tableIndex = 0;
+                                               ordering.forEach( i => {
+                                                       currentTable.push(activePlayers[i].index);
+                                                       if (currentTable.length == repartition[tableIndex])
                                                        {
-                                                               let tblNums = _.sample(_.range(tables.length), missingPlayers);
-                                                               tblNums.forEach( num => {
-                                                                       currentTable.push(tables[num].pop());
+                                                               if (currentTable.length == 3)
+                                                                       currentTable.push(0); //add Toto
+                                                               // flush
+                                                               tables.push(currentTable);
+                                                               currentTable = [];
+                                                               tableIndex++;
+                                                       }
+                                               });
+                                       }
+                                       else
+                                       {
+                                               // General case after round 1:
+                                               // NOTE: alternative method, deterministic: player 1 never move, player 2 moves by 1, ...and so on
+                                               // --> but this leads to inferior pairings (e.g. 2 tables 8 players)
+                                               // -----
+                                               // 2bis) Compute the "meeting" matrix: who played who and how many times
+                                               let meetMat = _.range(this.players.length).map( i => {
+                                                       return _.times(this.players.length, _.constant(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=i+1; j<4; j++)
+                                                                               meetMat[t[i]][t[j]]++;
+                                                               }
+                                                       });
+                                               });
+                                               // 3) Fill tables by minimizing row sums of meetMat
+                                               const playersCount = activePlayers.length;
+                                               repartition.forEach( r => {
+                                                       // Pick first player at random among active players, unless there is one unpaired guy
+                                                       let firstPlayer = this.unpaired[0]; //can be undefined
+                                                       if (!firstPlayer || activePlayers.length < playersCount)
+                                                       {
+                                                               let randIndex = _.sample( _.range(activePlayers.length) );
+                                                               firstPlayer = activePlayers[randIndex].index;
+                                                               activePlayers.splice(randIndex, 1);
+                                                       }
+                                                       else
+                                                               activePlayers.splice( activePlayers.findIndex( item => { return item.index == firstPlayer; }), 1 );
+                                                       let table = [ firstPlayer ];
+                                                       for (let i=1; i<r; i++)
+                                                       {
+                                                               // Minimize row sums of meetMat for remaining players
+                                                               let counts = [];
+                                                               activePlayers.forEach( u => {
+                                                                       let count = 0;
+                                                                       let candidate = u.index;
+                                                                       table.forEach( p => {
+                                                                               count += meetMat[p][candidate];
+                                                                               count += meetMat[candidate][p];
+                                                                       });
+                                                                       counts.push( {index:u.index, count:count } );
                                                                });
+                                                               counts.sort( (a,b) => { return a.count - b.count; });
+                                                               table.push(counts[0].index);
+                                                               activePlayers.splice( activePlayers.findIndex( item => { return item.index == counts[0].index; }), 1 );
                                                        }
-                                               }
-                                               if (currentTable.length >= 3)
-                                                       tables.push(currentTable);
-                                               else
-                                                       this.unpaired = currentTable;
+                                                       if (table.length == 3)
+                                                               table.push(0); //add Todo
+                                                       tables.push(table);
+                                               });
                                        }
-                                       // Ensure that all tables have 4 players
-                                       tables.forEach( t => {
-                                               if (t.length < 4)
-                                                       t.push(0); //index of "Toto", ghost player
-                                       });
+                                       if (tables.length >= 1 && tables[tables.length-1].length < 3)
+                                               this.unpaired = tables.pop();
+                                       else
+                                               this.unpaired = [];
                                        this.tables = tables;
-                                       this.sessions = tables.map( t => { return []; }); //empty sessions
-                                       this.scored = tables.map( t => { return false; }); //nothing scored yet
-                                       this.currentIndex = -1; //required if reset while scoring
+                                       this.resetScores();
+                               },
+                               resetScores: function() {
+                                       this.sessions = this.tables.map( t => { return []; }); //empty sessions
+                                       this.scored = this.tables.map( t => { return false; }); //nothing scored yet
                                },
                                showScoreForm: function(table,index) {
                                        if (this.sessions[index].length == 0)
@@ -217,7 +307,7 @@ new Vue({
                                        Vue.set(this.scored, this.currentIndex, true);
                                        this.currentIndex = -1;
                                },
-                               resetScore: function() {
+                               cancelScore: function() {
                                        let pdts = this.getPdts();
                                        for (let i=0; i<4; i++)
                                        {
@@ -233,13 +323,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: `
                                <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}">
-                                       <div @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}">
+                                       <div v-show="!setter" @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}">
                                                {{ formattedTime }}
                                        </div>
+                                       <input type="text" autofocus id="setter" @keyup.enter="setTime()" @keyup.esc="setter=false" v-show="setter" v-model="setterTime"></input>
                                        <img class="close-cross" src="img/cross.svg" @click="$emit('clockover')"/>
                                </div>
                        `,
@@ -257,6 +351,11 @@ new Vue({
                                },
                        },
                        methods: {
+                               setTime: function() {
+                                       this.initialTime = this.setterTime;
+                                       this.setter = false;
+                                       this.reset();
+                               },
                                padToZero: function(a) {
                                        if (a < 10)
                                                return "0" + a;
@@ -269,7 +368,7 @@ new Vue({
                                },
                                reset: function(e) {
                                        this.running = false;
-                                       this.time = 5400; //1:30
+                                       this.time = this.initialTime * 60;
                                },
                                start: function() {
                                        if (!this.running)
@@ -280,6 +379,8 @@ new Vue({
                                                this.running = false;
                                                return;
                                        }
+                                       if (this.time == this.initialTime * 60)
+                                               new Audio("sounds/gong.mp3").play(); //gong at the beginning
                                        setTimeout(() => {
                                                if (this.running)
                                                        this.time--;
@@ -288,8 +389,27 @@ 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','commitScores'],
@@ -408,7 +528,11 @@ new Vue({
                                .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};
+                                       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;