last requirements implemented; still a 'restore' bug to fix
[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-ranking': {
49 props: ['players','sortByScore','writeScoreToDb'],
50 template: `
51 <div id="ranking">
52 <table class="ranking">
53 <tr class="title">
54 <th>Rang</th>
55 <th>Joueur</th>
56 <th>Points</th>
57 <th>Mini-pts</th>
58 </tr>
59 <tr v-for="p in sortedPlayers">
60 <td>{{ p.rank }}</td>
61 <td>{{ p.prenom }} {{ p.nom }}</td>
62 <td>{{ p.pdt }}</td>
63 <td>{{ p.session }}</td>
64 </tr>
65 </table>
66 <div class="button-container-vertical" style="width:200px">
67 <button class="btn cancel" @click="resetPlayers()">Réinitialiser</button>
68 <button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button>
69 </div>
70 </div>
71 `,
72 computed: {
73 sortedPlayers: function() {
74 let res = this.rankPeople();
75 // Add rank information (taking care of ex-aequos)
76 let rank = 1;
77 for (let i=0; i<res.length; i++)
78 {
79 if (i==0 || this.sortByScore(res[i],res[i-1]) == 0)
80 res[i].rank = rank;
81 else //strictly lower scoring
82 res[i].rank = ++rank;
83 }
84 return res;
85 },
86 },
87 methods: {
88 rankPeople: function() {
89 return this.players
90 .slice(1) //discard Toto
91 .map( p => { return Object.assign({}, p); }) //to not alter original array
92 .sort(this.sortByScore);
93 },
94 resetPlayers: function() {
95 this.players
96 .slice(1) //discard Toto
97 .forEach( p => {
98 p.pdt = 0;
99 p.session = 0;
100 p.available = 1;
101 });
102 this.writeScoreToDb();
103 document.getElementById("runPairing").click(); //TODO: hack...
104 },
105 restoreLast: function() {
106 let xhr = new XMLHttpRequest();
107 let self = this;
108 xhr.onreadystatechange = function() {
109 if (this.readyState == 4 && this.status == 200)
110 {
111 let players = JSON.parse(xhr.responseText);
112 if (players.length > 0)
113 {
114 players.unshift({ //add ghost 4th player for 3-players tables
115 prenom: "Toto",
116 nom: "",
117 pdt: 0,
118 session: 0,
119 available: 0,
120 });
121 self.players = players;
122 }
123 }
124 };
125 xhr.open("GET", "scripts/rw_players.php?restore=1", true);
126 xhr.send(null);
127 },
128 },
129 },
130 'my-pairings': {
131 props: ['players','writeScoreToDb'],
132 data: function() {
133 return {
134 unpaired: [],
135 tables: [], //array of arrays of players indices
136 sessions: [], //"mini-points" for each table
137 currentIndex: -1, //table index for scoring
138 scored: [], //boolean for each table index
139 };
140 },
141 template: `
142 <div id="pairings">
143 <div v-show="currentIndex < 0">
144 <button id="runPairing" class="block btn" @click="doPairings()">Nouvelle ronde</button>
145 <div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}"
146 @click="showScoreForm(table,index)">
147 <p>Table {{ index+1 }}</p>
148 <table>
149 <tr v-for="(i,j) in table">
150 <td :class="{toto: players[i].prenom=='Toto'}">{{ players[i].prenom }} {{ players[i].nom }}</td>
151 <td class="score"><span v-show="sessions[index].length > 0">{{ sessions[index][j] }}</span></td>
152 </tr>
153 </table>
154 </div>
155 <div v-if="unpaired.length>0" class="pairing unpaired">
156 <p>Exempts</p>
157 <div v-for="i in unpaired">
158 {{ players[i].prenom }} {{ players[i].nom }}
159 </div>
160 </div>
161 </div>
162 <div id="scoreInput" v-if="currentIndex >= 0">
163 <table>
164 <tr v-for="(index,i) in tables[currentIndex]">
165 <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
166 {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
167 </td>
168 <td><input type="text" v-model="sessions[currentIndex][i]" value="0"/></td>
169 </tr>
170 </table>
171 <div class="button-container-horizontal">
172 <button class="btn validate" @click="setScore()">Enregistrer</button>
173 <button class="btn" @click="currentIndex = -1">Fermer</button>
174 </div>
175 <div v-if="scored[currentIndex]" class="warning">
176 Attention: un score a déjà été enregistré.
177 Les points indiqués ici s'ajouteront : il faut d'abord
178 <span class="link" @click="document.getElementById('restoreBtn').click()">restaurer l'état précédent.</span>
179 Si c'est déjà fait, ignorer ce message :)
180 </div>
181 </div>
182 </div>
183 `,
184 methods: {
185 doPairings: function() {
186 // Simple case first: 4 by 4
187 let tables = [];
188 let currentTable = [];
189 let ordering = _.shuffle(_.range(this.players.length));
190 for (let i=0; i<ordering.length; i++)
191 {
192 if ( ! this.players[ordering[i]].available )
193 continue;
194 if (currentTable.length >= 4)
195 {
196 tables.push(currentTable);
197 currentTable = [];
198 }
199 currentTable.push(ordering[i]);
200 }
201 // Analyse remainder
202 this.unpaired = [];
203 if (currentTable.length != 0)
204 {
205 if (currentTable.length < 3)
206 {
207 let missingPlayers = 3 - currentTable.length;
208 // Pick players from 'missingPlayers' random different tables, if possible
209 if (tables.length >= missingPlayers)
210 {
211 let tblNums = _.sample(_.range(tables.length), missingPlayers);
212 tblNums.forEach( num => {
213 currentTable.push(tables[num].pop());
214 });
215 }
216 }
217 if (currentTable.length >= 3)
218 tables.push(currentTable);
219 else
220 this.unpaired = currentTable;
221 }
222 // Ensure that all tables have 4 players
223 tables.forEach( t => {
224 if (t.length < 4)
225 t.push(0); //index of "Toto", ghost player
226 });
227 this.tables = tables;
228 this.sessions = tables.map( t => { return []; }); //empty sessions
229 this.scored = tables.map( t => { return false; }); //nothing scored yet
230 this.currentIndex = -1; //required if reset while scoring
231 },
232 showScoreForm: function(table,index) {
233 if (this.sessions[index].length == 0)
234 this.sessions[index] = _.times(table.length, _.constant(0));
235 this.currentIndex = index;
236 },
237 setScore: function() {
238 let sortedSessions = this.sessions[this.currentIndex]
239 .map( (s,i) => { return {value:s, index:i}; })
240 .sort( (a,b) => { return parseInt(b.value) - parseInt(a.value); });
241 let pdts = [4, 2, 1, 0];
242 // NOTE: take care of ex-aequos (spread points subtotal)
243 let curSum = 0, curCount = 0, start = 0;
244 for (let i=0; i<4; i++)
245 {
246 // Update pdts:
247 curSum += pdts[i];
248 curCount++;
249 if (i==3 || sortedSessions[i].value > sortedSessions[i+1].value)
250 {
251 let pdt = curSum / curCount;
252 for (let j=start; j<=i; j++)
253 this.players[this.tables[this.currentIndex][sortedSessions[j].index]].pdt += pdt;
254 curSum = 0;
255 curCount = 0;
256 start = i+1;
257 }
258 // Update sessions:
259 this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]);
260 }
261 this.scored[this.currentIndex] = true;
262 this.currentIndex = -1;
263 this.writeScoreToDb();
264 },
265 },
266 },
267 },
268 created: function() {
269 let xhr = new XMLHttpRequest();
270 let self = this;
271 xhr.onreadystatechange = function() {
272 if (this.readyState == 4 && this.status == 200)
273 {
274 let players = JSON.parse(xhr.responseText);
275 players.forEach( p => {
276 p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0;
277 p.session = !!p.session ? parseInt(p.session) : 0;
278 p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
279 });
280 players.unshift({ //add ghost 4th player for 3-players tables
281 prenom: "Toto",
282 nom: "",
283 pdt: 0,
284 session: 0,
285 available: 0,
286 });
287 self.players = players;
288 }
289 };
290 xhr.open("GET", "scripts/rw_players.php", true);
291 xhr.send(null);
292 },
293 methods: {
294 // Used both in ranking and pairings:
295 sortByScore: function(a,b) {
296 return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2;
297 },
298 writeScoreToDb: function() {
299 let xhr = new XMLHttpRequest();
300 xhr.open("POST", "scripts/rw_players.php");
301 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
302 let orderedPlayers = this.players
303 .slice(1) //discard Toto
304 .map( p => { return Object.assign({}, p); }) //deep (enough) copy
305 .sort(this.sortByScore);
306 xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
307 },
308 },
309 });