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(); | ||