improve documentation
[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:s, index:i}; })
158 .sort( (a,b) => { return parseInt(b.value) - parseInt(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: textHeight + 'px', fontSize: 0.66*textHeight + '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 textHeight: function() {
210 return screen.height;
211 },
212 },
213 methods: {
214 padToZero: function(a) {
215 if (a < 10)
216 return "0" + a;
217 return a;
218 },
219 pauseResume: function() {
220 this.running = !this.running;
221 if (this.running)
222 this.start();
223 },
224 reset: function(e) {
225 this.running = false;
226 this.time = 5400; //1:30
227 },
228 start: function() {
229 if (!this.running)
230 return;
231 if (this.time == 0)
232 {
233 new Audio("sounds/gong.mp3").play();
234 this.running = false;
235 return;
236 }
237 setTimeout(() => {
238 if (this.running)
239 this.time--;
240 this.start();
241 }, 1000);
242 },
243 },
244 created: function() {
245 this.reset();
246 },
247 },
248 'my-ranking': {
249 props: ['players','sortByScore','writeScoreToDb'],
250 template: `
251 <div id="ranking">
252 <table class="ranking">
253 <tr class="title">
254 <th>Rang</th>
255 <th>Joueur</th>
256 <th>Points</th>
257 <th>Mini-pts</th>
258 </tr>
259 <tr v-for="p in sortedPlayers">
260 <td>{{ p.rank }}</td>
261 <td>{{ p.prenom }} {{ p.nom }}</td>
262 <td>{{ p.pdt }}</td>
263 <td>{{ p.session }}</td>
264 </tr>
265 </table>
266 <div class="button-container-vertical" style="width:200px">
267 <button class="btn cancel" @click="resetPlayers()">Réinitialiser</button>
268 <button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button>
269 </div>
270 </div>
271 `,
272 computed: {
273 sortedPlayers: function() {
274 let res = this.rankPeople();
275 // Add rank information (taking care of ex-aequos)
276 let rank = 1;
277 for (let i=0; i<res.length; i++)
278 {
279 if (i==0 || this.sortByScore(res[i],res[i-1]) == 0)
280 res[i].rank = rank;
281 else //strictly lower scoring
282 res[i].rank = ++rank;
283 }
284 return res;
285 },
286 },
287 methods: {
288 rankPeople: function() {
289 return this.players
290 .slice(1) //discard Toto
291 .sort(this.sortByScore);
292 },
293 resetPlayers: function() {
294 this.players
295 .slice(1) //discard Toto
296 .forEach( p => {
297 p.pdt = 0;
298 p.session = 0;
299 p.available = 1;
300 });
301 this.writeScoreToDb();
302 document.getElementById("runPairing").click();
303 },
304 restoreLast: function() {
305 let xhr = new XMLHttpRequest();
306 let self = this;
307 xhr.onreadystatechange = function() {
308 if (this.readyState == 4 && this.status == 200)
309 {
310 let players = JSON.parse(xhr.responseText);
311 if (players.length > 0)
312 {
313 players.unshift({ //add ghost 4th player for 3-players tables
314 prenom: "Toto",
315 nom: "",
316 pdt: 0,
317 session: 0,
318 available: 0,
319 });
320 // NOTE: Vue warning "do not mutate property" if direct self.players = players
321 for (let i=1; i<players.length; i++)
322 {
323 players[i].pdt = parseFloat(players[i].pdt);
324 players[i].session = parseInt(players[i].session);
325 Vue.set(self.players, i, players[i]);
326 }
327 }
328 }
329 };
330 xhr.open("GET", "scripts/rw_players.php?restore=1", true);
331 xhr.send(null);
332 },
333 },
334 },
335 },
336 created: function() {
337 let xhr = new XMLHttpRequest();
338 let self = this;
339 xhr.onreadystatechange = function() {
340 if (this.readyState == 4 && this.status == 200)
341 {
342 let players = JSON.parse(xhr.responseText);
343 players.forEach( p => {
344 p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0;
345 p.session = !!p.session ? parseInt(p.session) : 0;
346 p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
347 });
348 players.unshift({ //add ghost 4th player for 3-players tables
349 prenom: "Toto",
350 nom: "",
351 pdt: 0,
352 session: 0,
353 available: 0,
354 });
355 self.players = players;
356 }
357 };
358 xhr.open("GET", "scripts/rw_players.php", true);
359 xhr.send(null);
360 },
361 methods: {
362 // Used both in ranking and pairings:
363 sortByScore: function(a,b) {
364 return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2;
365 },
366 writeScoreToDb: function() {
367 let xhr = new XMLHttpRequest();
368 xhr.open("POST", "scripts/rw_players.php");
369 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
370 let orderedPlayers = this.players
371 .slice(1) //discard Toto
372 .sort(this.sortByScore);
373 xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
374 },
375 },
376 });