diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2025-05-26 15:55:29 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2025-05-26 15:55:29 +0200 |
| commit | 47cb23ce1f991c21ceb9273cf4bed717a09abd9a (patch) | |
| tree | 90f6e3299abf5ec632123513fc80959f3336e15e /tabularasa.js | |
Kickoff commit
Diffstat (limited to 'tabularasa.js')
| -rw-r--r-- | tabularasa.js | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/tabularasa.js b/tabularasa.js new file mode 100644 index 0000000..ee45493 --- /dev/null +++ b/tabularasa.js | |||
| @@ -0,0 +1,284 @@ | |||
| 1 | // let API = "http://localhost:8080/example.json"; | ||
| 2 | let API = "example.json"; | ||
| 3 | |||
| 4 | var tabelle = [] /* Will contain all pairings */ | ||
| 5 | var leagues = [] /* List of league ids */ | ||
| 6 | var max_spieltage = 0; | ||
| 7 | var next_pairing_id = 0; | ||
| 8 | |||
| 9 | function weekday_to_string(weekday) { | ||
| 10 | return new Date(Date.UTC(1970, 0, 6+weekday)).toLocaleDateString(undefined, { weekday: 'long' }); | ||
| 11 | } | ||
| 12 | |||
| 13 | var wildcard = JSON.parse('{"id":"-1", "name":"*"}'); | ||
| 14 | var nowhere = JSON.parse('{"id":"-1", "name":"*"}'); | ||
| 15 | |||
| 16 | class paarung { | ||
| 17 | static _next_id = 0; | ||
| 18 | constructor(team_a, team_b, league, ort) { | ||
| 19 | this.id = next_pairing_id++; | ||
| 20 | this.team_a = team_a; | ||
| 21 | this.team_b = team_b; | ||
| 22 | this.league = league; | ||
| 23 | this.ort = ort; | ||
| 24 | } | ||
| 25 | |||
| 26 | get weekday() { | ||
| 27 | if (this.team_a.id != -1 && this.team_b.id != -1) | ||
| 28 | return weekday_to_string(this.team_a.tag); | ||
| 29 | return "*"; | ||
| 30 | } | ||
| 31 | |||
| 32 | get spieltag() { | ||
| 33 | if (!this.spieltage.size) | ||
| 34 | return -1; | ||
| 35 | return [...this.spieltage][0]; | ||
| 36 | } | ||
| 37 | |||
| 38 | get name() { | ||
| 39 | return this.team_a.name + " :: " + this.team_b.name + ", Liga " + this.league; | ||
| 40 | } | ||
| 41 | }; | ||
| 42 | |||
| 43 | function is_same(team_a, team_b) { | ||
| 44 | if (team_a.id == -1 || team_b.id == -1 || team_a.id != team_b.id) | ||
| 45 | return false; | ||
| 46 | return true; | ||
| 47 | } | ||
| 48 | |||
| 49 | function is_same_ort(ort_a, ort_b) { | ||
| 50 | if (ort_a.id == -1 || ort_b.id == -1 || ort_a.id != ort_b.id) | ||
| 51 | return false; | ||
| 52 | return true; | ||
| 53 | } | ||
| 54 | |||
| 55 | function createTextElement(name, text) { | ||
| 56 | let elem = document.createElement(name); | ||
| 57 | elem.appendChild(document.createTextNode(text)); | ||
| 58 | return elem; | ||
| 59 | } | ||
| 60 | |||
| 61 | function appendChildList(elem, name, ...texts) { | ||
| 62 | for (const text of texts) | ||
| 63 | elem.appendChild(createTextElement(name, text)); | ||
| 64 | } | ||
| 65 | |||
| 66 | function draw_table() { | ||
| 67 | /* Draw tables for each league */ | ||
| 68 | let anchor = document.getElementById("anchor"); | ||
| 69 | for (league of leagues) { | ||
| 70 | anchor.appendChild(createTextElement("h2", "Liga " + league.toString())); | ||
| 71 | |||
| 72 | let table = document.createElement("table"); | ||
| 73 | let thead = document.createElement("thead"); | ||
| 74 | let tr = document.createElement("tr"); | ||
| 75 | appendChildList(tr, "th", "Heim", "Gäste", "Wochentag", "Tisch", "Spieltag"); | ||
| 76 | thead.appendChild(tr); | ||
| 77 | table.appendChild(thead); | ||
| 78 | |||
| 79 | for (paar of tabelle.filter(paar => paar.league == league).sort((paar_a, paar_b) => { if (!paar_a.spieltage.size || !paar_b.spieltage.size) return -1; return paar_a.spieltag - paar_b.spieltag } )) { | ||
| 80 | let tr = document.createElement("tr"); | ||
| 81 | appendChildList(tr, "td", paar.team_a.name, paar.team_b.name, paar.weekday, paar.ort.name, 1 + paar.spieltag); | ||
| 82 | |||
| 83 | table.appendChild(tr); | ||
| 84 | } | ||
| 85 | anchor.appendChild(table); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | function fill_table(data) { | ||
| 90 | leagues = [...new Set(data.teams.map(team => team.liga))].sort(); | ||
| 91 | |||
| 92 | /* Init some objects */ | ||
| 93 | for (ort of data.orte) | ||
| 94 | ort.pairings = new Array(); | ||
| 95 | |||
| 96 | /* Count necessary spieltage */ | ||
| 97 | for (league of leagues) { | ||
| 98 | let pairings_count = data.teams.filter(team => team.liga == league).sort((a,b) => a.id > b.id).length; | ||
| 99 | if (pairings_count % 2) | ||
| 100 | pairings_count++; | ||
| 101 | max_spieltage = Math.max(2 * (pairings_count - 1), max_spieltage); | ||
| 102 | } | ||
| 103 | |||
| 104 | /* Fill out complete table for all leagues */ | ||
| 105 | for (league of leagues) { | ||
| 106 | let league_teams = data.teams.filter(team => team.liga == league).sort((a,b) => a.id > b.id); | ||
| 107 | console.log( "liga " + league_teams.length ); | ||
| 108 | |||
| 109 | for (team_a of league_teams) { | ||
| 110 | var game_count = 0; | ||
| 111 | for (team_b of league_teams) { | ||
| 112 | if (team_a == team_b) | ||
| 113 | continue; | ||
| 114 | |||
| 115 | /* If Heimspiel-Team is smokers and the Gastteam is not, the alternative | ||
| 116 | location needs to be chosen */ | ||
| 117 | var ort = data.orte.find(ort => ort.id == team_a.heimort); | ||
| 118 | if (team_b.nichtraucher && ort.raucher) | ||
| 119 | ort = data.orte.find(ort => ort.id == team_a.ersatzort); | ||
| 120 | |||
| 121 | pair = new paarung(team_a, team_b, league, ort); | ||
| 122 | tabelle.push(pair); | ||
| 123 | ort.pairings.push(pair); | ||
| 124 | game_count++; | ||
| 125 | } | ||
| 126 | /* Fill rest of this team's games with wildcard games */ | ||
| 127 | while (game_count < max_spieltage / 2) { | ||
| 128 | tabelle.push(new paarung(team_a, wildcard, league, nowhere)); | ||
| 129 | tabelle.push(new paarung(wildcard, team_a, league, nowhere)); | ||
| 130 | game_count++; | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | /* Fill all leagues with uneven or fewer spieltage with wildcard games */ | ||
| 136 | for (league of leagues) { | ||
| 137 | |||
| 138 | } | ||
| 139 | |||
| 140 | /* Check if an ort is over-provisioned | ||
| 141 | for (ort of data.orte) { | ||
| 142 | for (weekday of [...new Set(ort.pairings.map(pair => pair.weekday))].sort()) { | ||
| 143 | var games_on_day = ort.pairings.filter(pair => pair.weekday = weekday); | ||
| 144 | if (games_on_day > max_spieltage) | ||
| 145 | alert("Ort " + ort.name + "over provisioned on weekday " + weekday_to_string(weekday)); | ||
| 146 | } | ||
| 147 | } */ | ||
| 148 | } | ||
| 149 | |||
| 150 | function shuffle_array(array) { | ||
| 151 | return array.map(value => ({ value, sort: Math.random() })) | ||
| 152 | .sort((a, b) => a.sort - b.sort) | ||
| 153 | .map(({ value }) => value); | ||
| 154 | } | ||
| 155 | |||
| 156 | function not_equal_function(s1, s2) { return s1 != s2; } | ||
| 157 | |||
| 158 | function find_csp_solution() { | ||
| 159 | var all_spieltage = new Array(); | ||
| 160 | for (let i=0; i < max_spieltage; i++) | ||
| 161 | all_spieltage.push(i); | ||
| 162 | |||
| 163 | for (league of leagues) { | ||
| 164 | var sub = tabelle.filter(paar => paar.league == league); | ||
| 165 | var candidate = {}, variables = {}, constraints = []; | ||
| 166 | for (outer of sub) { | ||
| 167 | variables[outer.id] = [...all_spieltage]; | ||
| 168 | for (inner of tabelle.filter(inner => | ||
| 169 | inner.id > outer.id && ( | ||
| 170 | is_same(outer.team_a, inner.team_a) || | ||
| 171 | is_same(outer.team_b, inner.team_a) || | ||
| 172 | is_same(outer.team_a, inner.team_b) || | ||
| 173 | is_same(outer.team_b, inner.team_b)))) | ||
| 174 | { | ||
| 175 | constraints.push([outer.id, inner.id, not_equal_function]); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | candidate.variables = variables; | ||
| 180 | candidate.constraints = constraints; | ||
| 181 | |||
| 182 | var result = csp.solve(candidate); | ||
| 183 | console.log(result); | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | |||
| 188 | function find_table_configuration() { | ||
| 189 | /* Check if there is one possible configuration that fulfills the following criteria: | ||
| 190 | 1) each team must have 0 or 1 games on any given week | ||
| 191 | 2) each location+weekday combo must have 0 or 1 games on any given week | ||
| 192 | 3) each pairing needs a week and a location | ||
| 193 | */ | ||
| 194 | var candidates = tabelle; /* Take a copy of the array */ | ||
| 195 | |||
| 196 | /* Add all possible spieltage to the list for | ||
| 197 | them to be later reduced */ | ||
| 198 | for (paar of candidates) { | ||
| 199 | paar.spieltage = new Set(); | ||
| 200 | for (let i=0; i < max_spieltage; i++) | ||
| 201 | paar.spieltage.add(i); | ||
| 202 | } | ||
| 203 | |||
| 204 | /* TODO: Add more initial constraints */ | ||
| 205 | |||
| 206 | while (candidates.length) { | ||
| 207 | /* First pick the pairings that have the least amount of spieltage to fit in */ | ||
| 208 | candidates = shuffle_array(candidates).sort((a, b) => a.spieltage.size <= b.spieltage.size); | ||
| 209 | |||
| 210 | var looking_at = candidates.pop(); | ||
| 211 | |||
| 212 | /* Pick random spieltag out of the possible ones */ | ||
| 213 | let spieltag = Array.from(looking_at.spieltage)[Math.floor(Math.random() * looking_at.spieltage.size)]; | ||
| 214 | |||
| 215 | /* Now we have to remove all new conflicting dates from other unset pairings: */ | ||
| 216 | |||
| 217 | /* Filter out all pairings on the same spieltag with the current team */ | ||
| 218 | for (paar of candidates.filter(paar => | ||
| 219 | is_same(paar.team_a, looking_at.team_a) || | ||
| 220 | is_same(paar.team_b, looking_at.team_a) || | ||
| 221 | is_same(paar.team_a, looking_at.team_b) || | ||
| 222 | is_same(paar.team_b, looking_at.team_b))) | ||
| 223 | { | ||
| 224 | paar.spieltage.delete(spieltag); | ||
| 225 | } | ||
| 226 | |||
| 227 | /* Filter out all pairing on the same spieltag with the same ort and weekday | ||
| 228 | for (paar of candidates.filter(paar => | ||
| 229 | is_same_ort(paar.ort, looking_at.ort) && | ||
| 230 | paar.weekday == looking_at.weekday)) | ||
| 231 | { | ||
| 232 | if (paar.ort.id != -1 && looking_at.ort.id != -1) | ||
| 233 | paar.spieltage.delete(spieltag); | ||
| 234 | } | ||
| 235 | */ | ||
| 236 | |||
| 237 | /* Filter out the reverse pairing until the second half of the season (Rueckrunde). | ||
| 238 | If it doesn't exist, we're probably already in Rueckrunde | ||
| 239 | var runden_start = Math.floor(max_spieltage * Math.floor(spieltag * 2 / max_spieltage) / 2); | ||
| 240 | for (paar of candidates.filter(paar => | ||
| 241 | is_same(paar.team_a, looking_at.team_b) || | ||
| 242 | is_same(paar.team_b, looking_at.team_a))) { | ||
| 243 | for (var exclude = 0; exclude < max_spieltage / 2; exclude++) | ||
| 244 | paar.spieltage.delete(runden_start + exclude); | ||
| 245 | } | ||
| 246 | */ | ||
| 247 | /* Filter out home rounds for the home team on next spieltag and guest rounds for | ||
| 248 | guest team on next spieltag, except for end of Hinrunde | ||
| 249 | if (spieltag != max_spieltage / 2 - 1) | ||
| 250 | for (paar of candidates.filter(paar => | ||
| 251 | is_same(paar.team_a, looking_at.team_a) || | ||
| 252 | is_same(paar.team_b, looking_at.team_b))) | ||
| 253 | paar.spieltage.delete(spieltag + 1); | ||
| 254 | */ | ||
| 255 | |||
| 256 | for (paar of candidates) | ||
| 257 | if (paar.spieltage.size == 0) { | ||
| 258 | console.log(candidates.length); | ||
| 259 | return false; | ||
| 260 | } | ||
| 261 | |||
| 262 | /* Now fix the date */ | ||
| 263 | looking_at.spieltage = new Set(); | ||
| 264 | looking_at.spieltage.add(spieltag); | ||
| 265 | } | ||
| 266 | return true; | ||
| 267 | } | ||
| 268 | |||
| 269 | function init() { | ||
| 270 | var xhttp = new XMLHttpRequest(); | ||
| 271 | xhttp.onreadystatechange = function() { | ||
| 272 | if (this.readyState == 4 && this.status == 200) { | ||
| 273 | fill_table(xhttp.response); | ||
| 274 | //find_table_configuration(); | ||
| 275 | find_csp_solution(); | ||
| 276 | draw_table(); | ||
| 277 | } | ||
| 278 | }; | ||
| 279 | xhttp.responseType = "json"; | ||
| 280 | xhttp.open("GET", API, true); | ||
| 281 | xhttp.send(); | ||
| 282 | } | ||
| 283 | |||
| 284 | init(); | ||
