rename scoring: follow french Mahjong federation conventions
[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 id="active">
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">
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'],
50 data: function() {
51 return {
52 sortMethod: "pdt",
53 };
54 },
55 template: `
56 <div id="ranking">
57 <table class="ranking">
58 <tr class="title">
59 <th>Rang</th>
60 <th>Joueur</th>
61 <th @click="sortMethod='pdt'" class="scoring" :class="{active: sortMethod=='pdt'}">Points</th>
62 <th @click="sortMethod='session'" class="scoring" :class="{active: sortMethod=='session'}">Mini-pts</th>
63 </tr>
64 <tr v-for="p in sortedPlayers" v-if="p.nom!=''">
65 <td>{{ p.rank }}</td>
66 <td>{{ p.prenom }} {{ p.nom }}</td>
67 <td>{{ p.pdt }}</td>
68 <td>{{ p.session }}</td>
69 </tr>
70 </table>
71 </div>
72 `,
73 computed: { //TODO: first sort on score, then on Pdt (and reciprocally) --> function add fraction relative Pdt / score (compute min max first, take care of 0 case)
74 sortedPlayers: function() {
75 let sortFunc = this.sortMethod == "pdt"
76 ? this.sortByPdt
77 : this.sortBySession;
78 let res = this.players
79 .map( p => { return Object.assign({}, p); }) //to not alter original array
80 .sort(sortFunc);
81 // Add rank information (taking care of ex-aequos)
82 let rank = 1;
83 for (let i=0; i<res.length; i++)
84 {
85 if (i==0 || sortFunc(res[i],res[i-1]) == 0)
86 res[i].rank = rank;
87 else //strictly lower scoring
88 res[i].rank = ++rank;
89 }
90 return res;
91 },
92 },
93 methods: {
94 sortByPdt: function(a,b) {
95 return b.pdt - a.pdt;
96 },
97 sortBySession: function(a,b) {
98 return b.session - a.session;
99 },
100 },
101 },
102 'my-pairings': {
103 props: ['players'],
104 data: function() {
105 return {
106 unpaired: [],
107 tables: [], //array of arrays of players indices
108 pdts: [], //"points de table" for each table
109 sessions: [], //"mini-points" for each table
110 currentIndex: -1, //table index for scoring
111 };
112 },
113 template: `
114 <div id="pairings">
115 <div v-show="currentIndex < 0">
116 <button class="block btn" @click="shuffle()">Appariement</button>
117 <div class="pairing" v-for="(table,index) in tables" :class="{scored: pdts[index].length > 0}"
118 @click="showScoreForm(table,index)">
119 <p>Table {{ index+1 }}</p>
120 <table>
121 <tr v-for="(i,j) in table">
122 <td :class="{toto: players[i].prenom=='Toto'}">{{ players[i].prenom }} {{ players[i].nom }}</td>
123 <td class="score"><span v-show="sessions[index].length > 0">{{ sessions[index][j] }}</span></td>
124 </tr>
125 </table>
126 </div>
127 <div v-if="unpaired.length>0" class="pairing unpaired">
128 <p>Exempts</p>
129 <div v-for="i in unpaired">
130 {{ players[i].prenom }} {{ players[i].nom }}
131 </div>
132 </div>
133 </div>
134 <div id="scoreInput" v-if="currentIndex >= 0">
135 <table>
136 <tr v-for="(index,i) in tables[currentIndex]">
137 <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
138 {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
139 </td>
140 <td><input type="text" v-model="sessions[currentIndex][i]" value="0"/></td>
141 </tr>
142 </table>
143 <div class="button-container">
144 <button class="btn" @click="setScore()">Enregistrer</button>
145 <button class="btn cancel" @click="resetScore()">Annuler</button>
146 </div>
147 </div>
148 </div>
149 `,
150 methods: {
151 doPairings: function() {
152 // Simple case first: 4 by 4
153 let tables = [];
154 let currentTable = [];
155 let ordering = _.shuffle(_.range(this.players.length)); //TODO: take scores into account?
156 for (let i=0; i<ordering.length; i++)
157 {
158 if ( ! this.players[ordering[i]].available )
159 continue;
160 if (currentTable.length >= 4)
161 {
162 tables.push(currentTable);
163 currentTable = [];
164 }
165 currentTable.push(ordering[i]);
166 }
167 // Analyse remainder
168 this.unpaired = [];
169 if (currentTable.length != 0)
170 {
171 if (currentTable.length < 3)
172 {
173 let missingPlayers = 3 - currentTable.length;
174 // Pick players from 'missingPlayers' random different tables, if possible
175 if (tables.length >= missingPlayers)
176 {
177 let tblNums = _.sample(_.range(tables.length), missingPlayers);
178 tblNums.forEach( num => {
179 currentTable.push(tables[num].pop());
180 });
181 }
182 }
183 if (currentTable.length >= 3)
184 tables.push(currentTable);
185 else
186 this.unpaired = currentTable;
187 }
188 // Ensure that all tables have 4 players
189 tables.forEach( t => {
190 if (t.length < 4)
191 t.push(0); //index of "Toto", ghost player
192 });
193 this.tables = tables;
194 this.pdts = tables.map( t => { return []; }); //empty pdts
195 this.sessions = tables.map( t => { return []; }); //empty sessions
196 },
197 shuffle: function() {
198 this.doPairings();
199 },
200 showScoreForm: function(table,index) {
201 if (this.pdts[index].length > 0)
202 return; //already scored
203 this.pdts[index] = _.times(table.length, _.constant(0));
204 this.sessions[index] = _.times(table.length, _.constant(0));
205 this.currentIndex = index;
206 },
207 setScore: function() {
208 let sortedSessions = this.sessions[this.currentIndex]
209 .map( (s,i) => { return {value:s, index:i}; })
210 .sort( (a,b) => { return parseInt(b.value) - parseInt(a.value); });
211 let pdts = [4, 2, 1, 0]; //TODO: ex-aequos ?!
212 for (let i=0; i<this.tables[this.currentIndex].length; i++)
213 {
214 this.players[this.tables[this.currentIndex][sortedSessions[i].index]].pdt += pdts[i];
215 this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]);
216 }
217 this.currentIndex = -1;
218 this.writeScoreToDb();
219 },
220 resetScore: function() {
221 this.pdts[this.currentIndex] = [];
222 this.sessions[this.currentIndex] = [];
223 this.currentIndex = -1;
224 },
225 writeScoreToDb: function()
226 {
227 let xhr = new XMLHttpRequest();
228 xhr.open("POST", "scripts/rw_players.php");
229 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
230 let orderedPlayers = this.players
231 .slice(1) //discard "Toto"
232 .map( p => { return Object.assign({}, p); }) //deep (enough) copy
233 .sort( (a,b) => { return b.pdt - a.pdt; }); //TODO: re-use sorting function in ranking component
234 xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
235 },
236 },
237 },
238 },
239 created: function() {
240 let xhr = new XMLHttpRequest();
241 let self = this;
242 xhr.onreadystatechange = function() {
243 if (this.readyState == 4 && this.status == 200)
244 {
245 let players = JSON.parse(xhr.responseText);
246 players.forEach( p => {
247 p.pdt = !!p.pdt ? parseInt(p.pdt) : 0;
248 p.session = !!p.session ? parseInt(p.session) : 0;
249 p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
250 });
251 players.unshift({ //add ghost 4th player for 3-players tables
252 prenom: "Toto",
253 nom: "",
254 pdt: 0,
255 session: 0,
256 available: 0,
257 });
258 self.players = players;
259 }
260 };
261 xhr.open("GET", "scripts/rw_players.php", true);
262 xhr.send(null);
263 },
264 });