Fixing attempt for timer position
[westcastle.git] / js / index.js
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">
12 <div class="left">
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>
21 <div id="inactive" class="right">
22 <p>Absents</p>
23 <table class="list">
24 <tr v-for="p in sortedPlayers" v-if="!p.available && p.nom!=''" @click="toggleAvailability(p.index)">
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 },
48 'my-pairings': {
49 props: ['players','writeScoreToDb'],
50 data: function() {
51 return {
52 unpaired: [],
53 tables: [], //array of arrays of players indices
54 sessions: [], //"mini-points" for each table
55 currentIndex: -1, //table index for scoring
56 scored: [], //boolean for each table index
57 };
58 },
59 template: `
60 <div id="pairings">
61 <div v-show="currentIndex < 0">
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]}"
64 @click="showScoreForm(table,index)">
65 <p>Table {{ index+1 }}</p>
66 <table>
67 <tr v-for="(i,j) in table">
68 <td :class="{toto: players[i].prenom=='Toto'}">{{ players[i].prenom }} {{ players[i].nom }}</td>
69 <td class="score"><span v-show="sessions[index].length > 0">{{ sessions[index][j] }}</span></td>
70 </tr>
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]">
83 <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
84 {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
85 </td>
86 <td><input type="text" v-model="sessions[currentIndex][i]"/></td>
87 </tr>
88 </table>
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
96 <span class="link" @click="clickRestore()">restaurer l'état précédent.</span>
97 Si c'est déjà fait, ignorer ce message :)
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 = [];
107 let ordering = _.shuffle(_.range(this.players.length));
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 }
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 });
145 this.tables = tables;
146 this.sessions = tables.map( t => { return []; }); //empty sessions
147 this.scored = tables.map( t => { return false; }); //nothing scored yet
148 this.currentIndex = -1; //required if reset while scoring
149 },
150 showScoreForm: function(table,index) {
151 if (this.sessions[index].length == 0)
152 this.sessions[index] = _.times(table.length, _.constant(0));
153 this.currentIndex = index;
154 },
155 setScore: function() {
156 let sortedSessions = this.sessions[this.currentIndex]
157 .map( (s,i) => { return {value:parseInt(s), index:i}; })
158 .sort( (a,b) => { return b.value - a.value; });
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++)
163 {
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:
177 this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]);
178 }
179 this.scored[this.currentIndex] = true;
180 this.currentIndex = -1;
181 this.writeScoreToDb();
182 },
183 clickRestore: function() {
184 document.getElementById('restoreBtn').click();
185 },
186 },
187 },
188 'my-timer': {
189 data: function() {
190 return {
191 time: 0, //remaining time, in seconds
192 running: false,
193 };
194 },
195 template: `
196 <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}">
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 },
209 divHeight: function() {
210 return screen.height;
211 },
212 divWidth: function() {
213 return screen.width;
214 },
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;
229 this.time = 5400; //1:30
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 },
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
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();
305 document.getElementById("runPairing").click();
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 });
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 }
330 }
331 }
332 };
333 xhr.open("GET", "scripts/rw_players.php?restore=1", true);
334 xhr.send(null);
335 },
336 },
337 },
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 => {
347 p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0;
348 p.session = !!p.session ? parseInt(p.session) : 0;
349 p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
350 });
351 players.unshift({ //add ghost 4th player for 3-players tables
352 prenom: "Toto",
353 nom: "",
354 pdt: 0,
355 session: 0,
356 available: 0,
357 });
358 self.players = players;
359 }
360 };
361 xhr.open("GET", "scripts/rw_players.php", true);
362 xhr.send(null);
363 },
364 methods: {
365 // Used both in ranking and pairings:
366 sortByScore: function(a,b) {
367 return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2;
368 },
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
375 .sort(this.sortByScore);
376 xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
377 },
378 },
379 });