summaryrefslogtreecommitdiff
path: root/tabularasa.js
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2025-05-26 15:55:29 +0200
committererdgeist <erdgeist@erdgeist.org>2025-05-26 15:55:29 +0200
commit47cb23ce1f991c21ceb9273cf4bed717a09abd9a (patch)
tree90f6e3299abf5ec632123513fc80959f3336e15e /tabularasa.js
Kickoff commit
Diffstat (limited to 'tabularasa.js')
-rw-r--r--tabularasa.js284
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";
2let API = "example.json";
3
4var tabelle = [] /* Will contain all pairings */
5var leagues = [] /* List of league ids */
6var max_spieltage = 0;
7var next_pairing_id = 0;
8
9function weekday_to_string(weekday) {
10 return new Date(Date.UTC(1970, 0, 6+weekday)).toLocaleDateString(undefined, { weekday: 'long' });
11}
12
13var wildcard = JSON.parse('{"id":"-1", "name":"*"}');
14var nowhere = JSON.parse('{"id":"-1", "name":"*"}');
15
16class 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
43function 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
49function 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
55function createTextElement(name, text) {
56 let elem = document.createElement(name);
57 elem.appendChild(document.createTextNode(text));
58 return elem;
59}
60
61function appendChildList(elem, name, ...texts) {
62 for (const text of texts)
63 elem.appendChild(createTextElement(name, text));
64}
65
66function 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
89function 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
150function 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
156function not_equal_function(s1, s2) { return s1 != s2; }
157
158function 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
188function 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
269function 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
284init();