4 players: [], //array of objects, filled later
9 props: ['players','initPlayers'],
15 <tr v-for="p in sortedPlayers" v-if="p.available" @click="toggleAvailability(p.index)">
16 <td>{{ p.prenom }}</td>
21 <div id="inactive" class="right">
24 <tr v-for="p in sortedPlayers" v-if="!p.available && p.nom!=''" @click="toggleAvailability(p.index)">
25 <td>{{ p.prenom }}</td>
31 <input class="hide" id="upload" type="file" @change="upload"/>
32 <button class="btn block cancel" @click="uploadTrigger()" title="Charge la liste des joueurs, en principe en début de tournoi">
39 sortedPlayers: function() {
41 .map( (p
,i
) => { return Object
.assign({}, p
, {index: i
}); })
43 return a
.nom
.localeCompare(b
.nom
);
48 toggleAvailability: function(i
) {
49 this.players
[i
].available
= 1 - this.players
[i
].available
;
51 uploadTrigger: function() {
52 document
.getElementById("upload").click();
55 let file
= (e
.target
.files
|| e
.dataTransfer
.files
)[0];
56 var reader
= new FileReader();
57 reader
.onloadend
= ev
=> {
58 this.initPlayers(ev
.currentTarget
.result
);
60 reader
.readAsText(file
);
65 props: ['players','commitScores'],
69 tables: [], //array of arrays of players indices
70 sessions: [], //"mini-points" for each table
71 currentIndex: -1, //table index for scoring
72 scored: [], //boolean for each table index
77 <div v-show="currentIndex < 0">
78 <div class="button-container-horizontal">
79 <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">
82 <button id="doPairings" class="btn" :class="{cancel: tables.length>0}" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
86 <div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}"
87 @click="showScoreForm(table,index)">
88 <p>Table {{ index+1 }}</p>
90 <tr v-for="(i,j) in table">
91 <td :class="{toto: players[i].prenom=='Toto'}">{{ players[i].prenom }} {{ players[i].nom }}</td>
92 <td class="score"><span v-show="sessions[index].length > 0">{{ sessions[index][j] }}</span></td>
96 <div v-if="unpaired.length>0" class="pairing unpaired">
98 <div v-for="i in unpaired">
99 {{ players[i].prenom }} {{ players[i].nom }}
103 <div id="scoreInput" v-if="currentIndex >= 0">
105 <tr v-for="(index,i) in tables[currentIndex]">
106 <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
107 {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
109 <td><input type="text" v-model="sessions[currentIndex][i]" :disabled="scored[currentIndex]"/></td>
112 <div class="button-container-horizontal">
113 <button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base (la rubrique Classement est mise à jour)">
116 <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">
119 <button class="btn" @click="closeScoreForm()">Fermer</button>
125 doPairings: function() {
126 // Simple case first: 4 by 4
128 let currentTable
= [];
129 let ordering
= _
.shuffle(_
.range(this.players
.length
));
130 for (let i
=0; i
<ordering
.length
; i
++)
132 if ( ! this.players
[ordering
[i
]].available
)
134 if (currentTable
.length
>= 4)
136 tables
.push(currentTable
);
139 currentTable
.push(ordering
[i
]);
143 if (currentTable
.length
!= 0)
145 if (currentTable
.length
< 3)
147 let missingPlayers
= 3 - currentTable
.length
;
148 // Pick players from 'missingPlayers' random different tables, if possible
149 if (tables
.length
>= missingPlayers
)
151 let tblNums
= _
.sample(_
.range(tables
.length
), missingPlayers
);
152 tblNums
.forEach( num
=> {
153 currentTable
.push(tables
[num
].pop());
157 if (currentTable
.length
>= 3)
158 tables
.push(currentTable
);
160 this.unpaired
= currentTable
;
162 // Ensure that all tables have 4 players
163 tables
.forEach( t
=> {
165 t
.push(0); //index of "Toto", ghost player
167 this.tables
= tables
;
168 this.sessions
= tables
.map( t
=> { return []; }); //empty sessions
169 this.scored
= tables
.map( t
=> { return false; }); //nothing scored yet
170 this.currentIndex
= -1; //required if reset while scoring
172 showScoreForm: function(table
,index
) {
173 if (this.sessions
[index
].length
== 0)
174 this.sessions
[index
] = _
.times(table
.length
, _
.constant(0));
175 this.currentIndex
= index
;
177 closeScoreForm: function() {
178 if (!this.scored
[this.currentIndex
])
179 this.sessions
[this.currentIndex
] = [];
180 this.currentIndex
= -1;
182 getPdts: function() {
183 let sortedSessions
= this.sessions
[this.currentIndex
]
184 .map( (s
,i
) => { return {value:parseInt(s
), index:i
}; })
185 .sort( (a
,b
) => { return b
.value
- a
.value
; });
186 const ref_pdts
= [4, 2, 1, 0];
187 // NOTE: take care of ex-aequos (spread points subtotal)
188 let curSum
= 0, curCount
= 0, start
= 0;
190 for (let i
=0; i
<4; i
++)
192 curSum
+= ref_pdts
[i
];
194 if (i
==3 || sortedSessions
[i
].value
> sortedSessions
[i
+1].value
)
196 let pdt
= curSum
/ curCount
;
197 for (let j
=start
; j
<=i
; j
++)
198 sortedPdts
.push(pdt
);
204 // Re-order pdts to match table order
205 let pdts
= [0, 0, 0, 0];
206 for (let i
=0; i
<4; i
++)
207 pdts
[sortedSessions
[i
].index
] = sortedPdts
[i
];
210 setScore: function() {
211 let pdts
= this.getPdts();
212 for (let i
=0; i
<4; i
++)
214 this.players
[this.tables
[this.currentIndex
][i
]].pdt
+= pdts
[i
];
215 this.players
[this.tables
[this.currentIndex
][i
]].session
+= parseInt(this.sessions
[this.currentIndex
][i
]);
217 Vue
.set(this.scored
, this.currentIndex
, true);
218 this.currentIndex
= -1;
220 resetScore: function() {
221 let pdts
= this.getPdts();
222 for (let i
=0; i
<4; i
++)
224 this.players
[this.tables
[this.currentIndex
][i
]].pdt
-= pdts
[i
];
225 this.players
[this.tables
[this.currentIndex
][i
]].session
-= parseInt(this.sessions
[this.currentIndex
][i
]);
227 Vue
.set(this.scored
, this.currentIndex
, false);
234 time: 0, //remaining time, in seconds
239 <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}">
240 <div @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}">
243 <img class="close-cross" src="img/cross.svg" @click="$emit('clockover')"/>
247 formattedTime: function() {
248 let seconds
= this.time
% 60;
249 let minutes
= Math
.floor(this.time
/ 60);
250 return this.padToZero(minutes
) + ":" + this.padToZero(seconds
);
252 divHeight: function() {
253 return screen
.height
;
255 divWidth: function() {
260 padToZero: function(a
) {
265 pauseResume: function() {
266 this.running
= !this.running
;
271 this.running
= false;
272 this.time
= 5400; //1:30
279 new Audio("sounds/gong.mp3").play();
280 this.running
= false;
290 created: function() {
295 props: ['players','sortByScore','commitScores'],
298 <table class="ranking">
305 <tr v-for="p in sortedPlayers">
306 <td>{{ p.rank }}</td>
307 <td>{{ p.prenom }} {{ p.nom }}</td>
309 <td>{{ p.session }}</td>
312 <div class="button-container-vertical" style="width:200px">
313 <a id="download" href="#"></a>
314 <button class="btn" @click="download()" title="Télécharge le classement courant au format CSV">Télécharger</button>
315 <button class="btn cancel" @click="resetPlayers()" title="Réinitialise les scores à zéro. ATTENTION: action irréversible">
322 sortedPlayers: function() {
323 let res
= this.rankPeople();
324 // Add rank information (taking care of ex-aequos)
326 for (let i
=0; i
<res
.length
; i
++)
328 if (i
==0 || this.sortByScore(res
[i
],res
[i
-1]) == 0)
330 else //strictly lower scoring
331 res
[i
].rank
= ++rank
;
337 rankPeople: function() {
339 .slice(1) //discard Toto
340 .sort(this.sortByScore
);
342 resetPlayers: function() {
343 if (confirm('Êtes-vous sûr ?'))
346 .slice(1) //discard Toto
353 document
.getElementById("doPairings").click();
356 download: function() {
357 // Prepare file content
358 let content
= "prénom,nom,pdt,session\n";
360 .slice(1) //discard Toto
361 .sort(this.sortByScore
)
363 content
+= p
.prenom
+ "," + p
.nom
+ "," + p
.pdt
+ "," + p
.session
+ "\n";
365 // Prepare and trigger download link
366 let downloadAnchor
= document
.getElementById("download");
367 downloadAnchor
.setAttribute("download", "classement.csv");
368 downloadAnchor
.href
= "data:text/plain;charset=utf-8," + encodeURIComponent(content
);
369 downloadAnchor
.click();
374 created: function() {
375 let players
= JSON
.parse(localStorage
.getItem("players"));
376 if (players
!== null)
378 this.addToto(players
);
379 this.players
= players
;
383 addToto: function(array
) {
384 array
.unshift({ //add ghost 4th player for 3-players tables
392 // Used both in ranking and pairings:
393 sortByScore: function(a
,b
) {
394 return b
.pdt
- a
.pdt
+ (Math
.atan(b
.session
- a
.session
) / (Math
.PI
/2)) / 2;
396 commitScores: function() {
397 localStorage
.setItem(
399 JSON
.stringify(this.players
.slice(1)) //discard Toto
402 // Used in players, reinit players array
403 initPlayers: function(csv
) {
405 .split(/\r\n|\n|\r/) //line breaks
406 .splice(1); //discard header
407 let players
= allLines
408 .filter( line
=> { return line
.length
> 0; }) //remove empty lines
410 let parts
= line
.split(",");
411 return {prenom: parts
[0], nom: parts
[1], pdt: 0, session:0, available: 1};
413 this.addToto(players
);
414 this.players
= players
;
415 this.commitScores(); //save players in memory