Commit | Line | Data |
---|---|---|
7a00c409 BA |
1 | new Vue({ |
2 | el: "#mahjong", | |
3 | data: { | |
4 | players: [], //array of objects, filled later | |
5 | display: "players", | |
6 | }, | |
7 | components: { | |
8 | 'my-players': { | |
9 | props: ['players'], | |
10 | template: ` | |
11 | <div id="players"> | |
1369a09d | 12 | <div class="left"> |
7a00c409 BA |
13 | <p>Présents</p> |
14 | <table class="list"> | |
15 | <tr v-for="p in sortedPlayers" v-if="p.available" @click="toggleAvailability(p.index)"> | |
16 | <td>{{ p.prenom }}</td> | |
17 | <td>{{ p.nom }}</td> | |
18 | </tr> | |
19 | </table> | |
20 | </div> | |
1369a09d | 21 | <div id="inactive" class="right"> |
7a00c409 BA |
22 | <p>Absents</p> |
23 | <table class="list"> | |
fd4a69e4 | 24 | <tr v-for="p in sortedPlayers" v-if="!p.available && p.nom!=''" @click="toggleAvailability(p.index)"> |
7a00c409 BA |
25 | <td>{{ p.prenom }}</td> |
26 | <td>{{ p.nom }}</td> | |
27 | </tr> | |
28 | </table> | |
29 | </div> | |
30 | </div> | |
31 | `, | |
32 | computed: { | |
33 | sortedPlayers: function() { | |
34 | return this.players | |
35 | .map( (p,i) => { return Object.assign({}, p, {index: i}); }) | |
36 | .sort( (a,b) => { | |
37 | return a.nom.localeCompare(b.nom); | |
38 | }); | |
39 | }, | |
40 | }, | |
41 | methods: { | |
42 | toggleAvailability: function(i) { | |
43 | this.players[i].available = 1 - this.players[i].available; | |
44 | this.$forceUpdate(); //TODO (Vue.set... ?!) | |
45 | }, | |
46 | }, | |
47 | }, | |
7a00c409 | 48 | 'my-pairings': { |
1369a09d | 49 | props: ['players','writeScoreToDb'], |
7a00c409 BA |
50 | data: function() { |
51 | return { | |
52 | unpaired: [], | |
53 | tables: [], //array of arrays of players indices | |
1d2d7593 | 54 | sessions: [], //"mini-points" for each table |
7a00c409 | 55 | currentIndex: -1, //table index for scoring |
1369a09d | 56 | scored: [], //boolean for each table index |
7a00c409 BA |
57 | }; |
58 | }, | |
59 | template: ` | |
60 | <div id="pairings"> | |
61 | <div v-show="currentIndex < 0"> | |
1369a09d BA |
62 | <button id="runPairing" class="block btn" @click="doPairings()">Nouvelle ronde</button> |
63 | <div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}" | |
7a00c409 BA |
64 | @click="showScoreForm(table,index)"> |
65 | <p>Table {{ index+1 }}</p> | |
66 | <table> | |
67 | <tr v-for="(i,j) in table"> | |
fd4a69e4 | 68 | <td :class="{toto: players[i].prenom=='Toto'}">{{ players[i].prenom }} {{ players[i].nom }}</td> |
1d2d7593 | 69 | <td class="score"><span v-show="sessions[index].length > 0">{{ sessions[index][j] }}</span></td> |
7a00c409 | 70 | </tr> |
7a00c409 BA |
71 | </table> |
72 | </div> | |
73 | <div v-if="unpaired.length>0" class="pairing unpaired"> | |
74 | <p>Exempts</p> | |
75 | <div v-for="i in unpaired"> | |
76 | {{ players[i].prenom }} {{ players[i].nom }} | |
77 | </div> | |
78 | </div> | |
79 | </div> | |
80 | <div id="scoreInput" v-if="currentIndex >= 0"> | |
81 | <table> | |
82 | <tr v-for="(index,i) in tables[currentIndex]"> | |
fd4a69e4 BA |
83 | <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}"> |
84 | {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }} | |
85 | </td> | |
6d7bb9ad | 86 | <td><input type="text" v-model="sessions[currentIndex][i]"/></td> |
7a00c409 BA |
87 | </tr> |
88 | </table> | |
1369a09d BA |
89 | <div class="button-container-horizontal"> |
90 | <button class="btn validate" @click="setScore()">Enregistrer</button> | |
91 | <button class="btn" @click="currentIndex = -1">Fermer</button> | |
92 | </div> | |
93 | <div v-if="scored[currentIndex]" class="warning"> | |
94 | Attention: un score a déjà été enregistré. | |
95 | Les points indiqués ici s'ajouteront : il faut d'abord | |
6d7bb9ad | 96 | <span class="link" @click="clickRestore()">restaurer l'état précédent.</span> |
1369a09d | 97 | Si c'est déjà fait, ignorer ce message :) |
7a00c409 BA |
98 | </div> |
99 | </div> | |
100 | </div> | |
101 | `, | |
102 | methods: { | |
103 | doPairings: function() { | |
104 | // Simple case first: 4 by 4 | |
105 | let tables = []; | |
106 | let currentTable = []; | |
ade10194 | 107 | let ordering = _.shuffle(_.range(this.players.length)); |
7a00c409 BA |
108 | for (let i=0; i<ordering.length; i++) |
109 | { | |
110 | if ( ! this.players[ordering[i]].available ) | |
111 | continue; | |
112 | if (currentTable.length >= 4) | |
113 | { | |
114 | tables.push(currentTable); | |
115 | currentTable = []; | |
116 | } | |
117 | currentTable.push(ordering[i]); | |
118 | } | |
119 | // Analyse remainder | |
120 | this.unpaired = []; | |
121 | if (currentTable.length != 0) | |
122 | { | |
123 | if (currentTable.length < 3) | |
124 | { | |
125 | let missingPlayers = 3 - currentTable.length; | |
126 | // Pick players from 'missingPlayers' random different tables, if possible | |
127 | if (tables.length >= missingPlayers) | |
128 | { | |
129 | let tblNums = _.sample(_.range(tables.length), missingPlayers); | |
130 | tblNums.forEach( num => { | |
131 | currentTable.push(tables[num].pop()); | |
132 | }); | |
133 | } | |
134 | } | |
135 | if (currentTable.length >= 3) | |
136 | tables.push(currentTable); | |
137 | else | |
138 | this.unpaired = currentTable; | |
139 | } | |
fd4a69e4 BA |
140 | // Ensure that all tables have 4 players |
141 | tables.forEach( t => { | |
142 | if (t.length < 4) | |
143 | t.push(0); //index of "Toto", ghost player | |
144 | }); | |
7a00c409 | 145 | this.tables = tables; |
1d2d7593 | 146 | this.sessions = tables.map( t => { return []; }); //empty sessions |
1369a09d BA |
147 | this.scored = tables.map( t => { return false; }); //nothing scored yet |
148 | this.currentIndex = -1; //required if reset while scoring | |
7a00c409 BA |
149 | }, |
150 | showScoreForm: function(table,index) { | |
1369a09d BA |
151 | if (this.sessions[index].length == 0) |
152 | this.sessions[index] = _.times(table.length, _.constant(0)); | |
7a00c409 BA |
153 | this.currentIndex = index; |
154 | }, | |
155 | setScore: function() { | |
1d2d7593 | 156 | let sortedSessions = this.sessions[this.currentIndex] |
ce0473b4 BA |
157 | .map( (s,i) => { return {value:parseInt(s), index:i}; }) |
158 | .sort( (a,b) => { return b.value - a.value; }); | |
1369a09d BA |
159 | let pdts = [4, 2, 1, 0]; |
160 | // NOTE: take care of ex-aequos (spread points subtotal) | |
161 | let curSum = 0, curCount = 0, start = 0; | |
162 | for (let i=0; i<4; i++) | |
7a00c409 | 163 | { |
1369a09d BA |
164 | // Update pdts: |
165 | curSum += pdts[i]; | |
166 | curCount++; | |
167 | if (i==3 || sortedSessions[i].value > sortedSessions[i+1].value) | |
168 | { | |
169 | let pdt = curSum / curCount; | |
170 | for (let j=start; j<=i; j++) | |
171 | this.players[this.tables[this.currentIndex][sortedSessions[j].index]].pdt += pdt; | |
172 | curSum = 0; | |
173 | curCount = 0; | |
174 | start = i+1; | |
175 | } | |
176 | // Update sessions: | |
1d2d7593 | 177 | this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]); |
7a00c409 | 178 | } |
1369a09d | 179 | this.scored[this.currentIndex] = true; |
7a00c409 BA |
180 | this.currentIndex = -1; |
181 | this.writeScoreToDb(); | |
182 | }, | |
6d7bb9ad BA |
183 | clickRestore: function() { |
184 | document.getElementById('restoreBtn').click(); | |
185 | }, | |
7a00c409 BA |
186 | }, |
187 | }, | |
2caa3889 BA |
188 | 'my-timer': { |
189 | data: function() { | |
190 | return { | |
191 | time: 0, //remaining time, in seconds | |
192 | running: false, | |
193 | }; | |
194 | }, | |
195 | template: ` | |
8d4d2300 | 196 | <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}"> |
2caa3889 BA |
197 | <div @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}"> |
198 | {{ formattedTime }} | |
199 | </div> | |
200 | <img class="close-cross" src="img/cross.svg" @click="$emit('clockover')"/> | |
201 | </div> | |
202 | `, | |
203 | computed: { | |
204 | formattedTime: function() { | |
205 | let seconds = this.time % 60; | |
206 | let minutes = Math.floor(this.time / 60); | |
207 | return this.padToZero(minutes) + ":" + this.padToZero(seconds); | |
208 | }, | |
8d4d2300 | 209 | divHeight: function() { |
6d7bb9ad BA |
210 | return screen.height; |
211 | }, | |
8d4d2300 BA |
212 | divWidth: function() { |
213 | return screen.width; | |
214 | }, | |
2caa3889 BA |
215 | }, |
216 | methods: { | |
217 | padToZero: function(a) { | |
218 | if (a < 10) | |
219 | return "0" + a; | |
220 | return a; | |
221 | }, | |
222 | pauseResume: function() { | |
223 | this.running = !this.running; | |
224 | if (this.running) | |
225 | this.start(); | |
226 | }, | |
227 | reset: function(e) { | |
228 | this.running = false; | |
48b3a536 | 229 | this.time = 5400; //1:30 |
2caa3889 BA |
230 | }, |
231 | start: function() { | |
232 | if (!this.running) | |
233 | return; | |
234 | if (this.time == 0) | |
235 | { | |
236 | new Audio("sounds/gong.mp3").play(); | |
237 | this.running = false; | |
238 | return; | |
239 | } | |
240 | setTimeout(() => { | |
241 | if (this.running) | |
242 | this.time--; | |
243 | this.start(); | |
244 | }, 1000); | |
245 | }, | |
246 | }, | |
247 | created: function() { | |
248 | this.reset(); | |
249 | }, | |
250 | }, | |
48b3a536 BA |
251 | 'my-ranking': { |
252 | props: ['players','sortByScore','writeScoreToDb'], | |
253 | template: ` | |
254 | <div id="ranking"> | |
255 | <table class="ranking"> | |
256 | <tr class="title"> | |
257 | <th>Rang</th> | |
258 | <th>Joueur</th> | |
259 | <th>Points</th> | |
260 | <th>Mini-pts</th> | |
261 | </tr> | |
262 | <tr v-for="p in sortedPlayers"> | |
263 | <td>{{ p.rank }}</td> | |
264 | <td>{{ p.prenom }} {{ p.nom }}</td> | |
265 | <td>{{ p.pdt }}</td> | |
266 | <td>{{ p.session }}</td> | |
267 | </tr> | |
268 | </table> | |
269 | <div class="button-container-vertical" style="width:200px"> | |
270 | <button class="btn cancel" @click="resetPlayers()">Réinitialiser</button> | |
271 | <button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button> | |
272 | </div> | |
273 | </div> | |
274 | `, | |
275 | computed: { | |
276 | sortedPlayers: function() { | |
277 | let res = this.rankPeople(); | |
278 | // Add rank information (taking care of ex-aequos) | |
279 | let rank = 1; | |
280 | for (let i=0; i<res.length; i++) | |
281 | { | |
282 | if (i==0 || this.sortByScore(res[i],res[i-1]) == 0) | |
283 | res[i].rank = rank; | |
284 | else //strictly lower scoring | |
285 | res[i].rank = ++rank; | |
286 | } | |
287 | return res; | |
288 | }, | |
289 | }, | |
290 | methods: { | |
291 | rankPeople: function() { | |
292 | return this.players | |
293 | .slice(1) //discard Toto | |
48b3a536 BA |
294 | .sort(this.sortByScore); |
295 | }, | |
296 | resetPlayers: function() { | |
297 | this.players | |
298 | .slice(1) //discard Toto | |
299 | .forEach( p => { | |
300 | p.pdt = 0; | |
301 | p.session = 0; | |
302 | p.available = 1; | |
303 | }); | |
304 | this.writeScoreToDb(); | |
6d7bb9ad | 305 | document.getElementById("runPairing").click(); |
48b3a536 BA |
306 | }, |
307 | restoreLast: function() { | |
308 | let xhr = new XMLHttpRequest(); | |
309 | let self = this; | |
310 | xhr.onreadystatechange = function() { | |
311 | if (this.readyState == 4 && this.status == 200) | |
312 | { | |
313 | let players = JSON.parse(xhr.responseText); | |
314 | if (players.length > 0) | |
315 | { | |
316 | players.unshift({ //add ghost 4th player for 3-players tables | |
317 | prenom: "Toto", | |
318 | nom: "", | |
319 | pdt: 0, | |
320 | session: 0, | |
321 | available: 0, | |
322 | }); | |
6d7bb9ad BA |
323 | // NOTE: Vue warning "do not mutate property" if direct self.players = players |
324 | for (let i=1; i<players.length; i++) | |
325 | { | |
326 | players[i].pdt = parseFloat(players[i].pdt); | |
327 | players[i].session = parseInt(players[i].session); | |
328 | Vue.set(self.players, i, players[i]); | |
329 | } | |
48b3a536 BA |
330 | } |
331 | } | |
332 | }; | |
333 | xhr.open("GET", "scripts/rw_players.php?restore=1", true); | |
334 | xhr.send(null); | |
335 | }, | |
336 | }, | |
337 | }, | |
7a00c409 BA |
338 | }, |
339 | created: function() { | |
340 | let xhr = new XMLHttpRequest(); | |
341 | let self = this; | |
342 | xhr.onreadystatechange = function() { | |
343 | if (this.readyState == 4 && this.status == 200) | |
344 | { | |
345 | let players = JSON.parse(xhr.responseText); | |
346 | players.forEach( p => { | |
1369a09d | 347 | p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0; |
1d2d7593 | 348 | p.session = !!p.session ? parseInt(p.session) : 0; |
7a00c409 BA |
349 | p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func |
350 | }); | |
fd4a69e4 BA |
351 | players.unshift({ //add ghost 4th player for 3-players tables |
352 | prenom: "Toto", | |
353 | nom: "", | |
fd4a69e4 | 354 | pdt: 0, |
1d2d7593 | 355 | session: 0, |
fd4a69e4 BA |
356 | available: 0, |
357 | }); | |
7a00c409 BA |
358 | self.players = players; |
359 | } | |
360 | }; | |
361 | xhr.open("GET", "scripts/rw_players.php", true); | |
362 | xhr.send(null); | |
363 | }, | |
ade10194 | 364 | methods: { |
1369a09d | 365 | // Used both in ranking and pairings: |
ade10194 BA |
366 | sortByScore: function(a,b) { |
367 | return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2; | |
368 | }, | |
1369a09d BA |
369 | writeScoreToDb: function() { |
370 | let xhr = new XMLHttpRequest(); | |
371 | xhr.open("POST", "scripts/rw_players.php"); | |
372 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | |
373 | let orderedPlayers = this.players | |
374 | .slice(1) //discard Toto | |
1369a09d BA |
375 | .sort(this.sortByScore); |
376 | xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers))); | |
377 | }, | |
ade10194 | 378 | }, |
7a00c409 | 379 | }); |