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