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 // TODO: clic sur "Valider" télécharge la ronde courante
126 // TODO: mémoriser les appariements passés pour éviter que les mêmes joueurs se rencontrent plusieurs fois
127 doPairings: function() {
128 // Simple case first: 4 by 4
130 let currentTable
= [];
131 let ordering
= _
.shuffle(_
.range(this.players
.length
));
132 for (let i
=0; i
<ordering
.length
; i
++)
134 if ( ! this.players
[ordering
[i
]].available
)
136 if (currentTable
.length
>= 4)
138 tables
.push(currentTable
);
141 currentTable
.push(ordering
[i
]);
145 if (currentTable
.length
!= 0)
147 if (currentTable
.length
< 3)
149 let missingPlayers
= 3 - currentTable
.length
;
150 // Pick players from 'missingPlayers' random different tables, if possible
151 if (tables
.length
>= missingPlayers
)
153 let tblNums
= _
.sample(_
.range(tables
.length
), missingPlayers
);
154 tblNums
.forEach( num
=> {
155 currentTable
.push(tables
[num
].pop());
159 if (currentTable
.length
>= 3)
160 tables
.push(currentTable
);
162 this.unpaired
= currentTable
;
164 // Ensure that all tables have 4 players
165 tables
.forEach( t
=> {
167 t
.push(0); //index of "Toto", ghost player
169 this.tables
= tables
;
170 this.sessions
= tables
.map( t
=> { return []; }); //empty sessions
171 this.scored
= tables
.map( t
=> { return false; }); //nothing scored yet
172 this.currentIndex
= -1; //required if reset while scoring
174 showScoreForm: function(table
,index
) {
175 if (this.sessions
[index
].length
== 0)
176 this.sessions
[index
] = _
.times(table
.length
, _
.constant(0));
177 this.currentIndex
= index
;
179 closeScoreForm: function() {
180 if (!this.scored
[this.currentIndex
])
181 this.sessions
[this.currentIndex
] = [];
182 this.currentIndex
= -1;
184 getPdts: function() {
185 let sortedSessions
= this.sessions
[this.currentIndex
]
186 .map( (s
,i
) => { return {value:parseInt(s
), index:i
}; })
187 .sort( (a
,b
) => { return b
.value
- a
.value
; });
188 const ref_pdts
= [4, 2, 1, 0];
189 // NOTE: take care of ex-aequos (spread points subtotal)
190 let curSum
= 0, curCount
= 0, start
= 0;
192 for (let i
=0; i
<4; i
++)
194 curSum
+= ref_pdts
[i
];
196 if (i
==3 || sortedSessions
[i
].value
> sortedSessions
[i
+1].value
)
198 let pdt
= curSum
/ curCount
;
199 for (let j
=start
; j
<=i
; j
++)
200 sortedPdts
.push(pdt
);
206 // Re-order pdts to match table order
207 let pdts
= [0, 0, 0, 0];
208 for (let i
=0; i
<4; i
++)
209 pdts
[sortedSessions
[i
].index
] = sortedPdts
[i
];
212 setScore: function() {
213 let pdts
= this.getPdts();
214 for (let i
=0; i
<4; i
++)
216 this.players
[this.tables
[this.currentIndex
][i
]].pdt
+= pdts
[i
];
217 this.players
[this.tables
[this.currentIndex
][i
]].session
+= parseInt(this.sessions
[this.currentIndex
][i
]);
219 Vue
.set(this.scored
, this.currentIndex
, true);
220 this.currentIndex
= -1;
222 resetScore: function() {
223 let pdts
= this.getPdts();
224 for (let i
=0; i
<4; i
++)
226 this.players
[this.tables
[this.currentIndex
][i
]].pdt
-= pdts
[i
];
227 this.players
[this.tables
[this.currentIndex
][i
]].session
-= parseInt(this.sessions
[this.currentIndex
][i
]);
229 Vue
.set(this.scored
, this.currentIndex
, false);
236 time: 0, //remaining time, in seconds
238 initialTime: 5400, //1h30
242 <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}">
243 <div @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}">
246 <img class="close-cross" src="img/cross.svg" @click="$emit('clockover')"/>
250 formattedTime: function() {
251 let seconds
= this.time
% 60;
252 let minutes
= Math
.floor(this.time
/ 60);
253 return this.padToZero(minutes
) + ":" + this.padToZero(seconds
);
255 divHeight: function() {
256 return screen
.height
;
258 divWidth: function() {
263 padToZero: function(a
) {
268 pauseResume: function() {
269 this.running
= !this.running
;
274 this.running
= false;
275 this.time
= this.initialTime
;
282 new Audio("sounds/gong.mp3").play();
283 this.running
= false;
286 if (this.time
== this.initialTime
)
287 new Audio("sounds/gong.mp3").play(); //gong at the beginning
295 created: function() {
300 props: ['players','sortByScore','commitScores'],
303 <table class="ranking">
310 <tr v-for="p in sortedPlayers">
311 <td>{{ p.rank }}</td>
312 <td>{{ p.prenom }} {{ p.nom }}</td>
314 <td>{{ p.session }}</td>
317 <div class="button-container-vertical" style="width:200px">
318 <a id="download" href="#"></a>
319 <button class="btn" @click="download()" title="Télécharge le classement courant au format CSV">Télécharger</button>
320 <button class="btn cancel" @click="resetPlayers()" title="Réinitialise les scores à zéro. ATTENTION: action irréversible">
327 sortedPlayers: function() {
328 let res
= this.rankPeople();
329 // Add rank information (taking care of ex-aequos)
331 for (let i
=0; i
<res
.length
; i
++)
333 if (i
==0 || this.sortByScore(res
[i
],res
[i
-1]) == 0)
335 else //strictly lower scoring
336 res
[i
].rank
= ++rank
;
342 rankPeople: function() {
344 .slice(1) //discard Toto
345 .sort(this.sortByScore
);
347 resetPlayers: function() {
348 if (confirm('Êtes-vous sûr ?'))
351 .slice(1) //discard Toto
358 document
.getElementById("doPairings").click();
361 download: function() {
362 // Prepare file content
363 let content
= "prénom,nom,pdt,session\n";
365 .slice(1) //discard Toto
366 .sort(this.sortByScore
)
368 content
+= p
.prenom
+ "," + p
.nom
+ "," + p
.pdt
+ "," + p
.session
+ "\n";
370 // Prepare and trigger download link
371 let downloadAnchor
= document
.getElementById("download");
372 downloadAnchor
.setAttribute("download", "classement.csv");
373 downloadAnchor
.href
= "data:text/plain;charset=utf-8," + encodeURIComponent(content
);
374 downloadAnchor
.click();
379 created: function() {
380 let players
= JSON
.parse(localStorage
.getItem("players"));
381 if (players
!== null)
383 this.addToto(players
);
384 this.players
= players
;
388 addToto: function(array
) {
389 array
.unshift({ //add ghost 4th player for 3-players tables
397 // Used both in ranking and pairings:
398 sortByScore: function(a
,b
) {
399 return b
.pdt
- a
.pdt
+ (Math
.atan(b
.session
- a
.session
) / (Math
.PI
/2)) / 2;
401 commitScores: function() {
402 localStorage
.setItem(
404 JSON
.stringify(this.players
.slice(1)) //discard Toto
407 // Used in players, reinit players array
408 initPlayers: function(csv
) {
410 .split(/\r\n|\n|\r/) //line breaks
411 .splice(1); //discard header
412 let players
= allLines
413 .filter( line
=> { return line
.length
> 0; }) //remove empty lines
415 let parts
= line
.split(",");
416 let p
= { prenom: parts
[0], nom: parts
[1] };
417 p
.pdt
= parts
.length
> 2 ? parseFloat(parts
[2]) : 0;
418 p
.session
= parts
.length
> 3 ? parseInt(parts
[3]) : 0;
419 p
.available
= parts
.length
> 4 ? parts
[4] : 1;
422 this.addToto(players
);
423 this.players
= players
;
424 this.commitScores(); //save players in memory