From 47cb23ce1f991c21ceb9273cf4bed717a09abd9a Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 26 May 2025 15:55:29 +0200 Subject: Kickoff commit --- tabularasa.js | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 tabularasa.js (limited to 'tabularasa.js') diff --git a/tabularasa.js b/tabularasa.js new file mode 100644 index 0000000..ee45493 --- /dev/null +++ b/tabularasa.js @@ -0,0 +1,284 @@ +// let API = "http://localhost:8080/example.json"; +let API = "example.json"; + +var tabelle = [] /* Will contain all pairings */ +var leagues = [] /* List of league ids */ +var max_spieltage = 0; +var next_pairing_id = 0; + +function weekday_to_string(weekday) { + return new Date(Date.UTC(1970, 0, 6+weekday)).toLocaleDateString(undefined, { weekday: 'long' }); +} + +var wildcard = JSON.parse('{"id":"-1", "name":"*"}'); +var nowhere = JSON.parse('{"id":"-1", "name":"*"}'); + +class paarung { + static _next_id = 0; + constructor(team_a, team_b, league, ort) { + this.id = next_pairing_id++; + this.team_a = team_a; + this.team_b = team_b; + this.league = league; + this.ort = ort; + } + + get weekday() { + if (this.team_a.id != -1 && this.team_b.id != -1) + return weekday_to_string(this.team_a.tag); + return "*"; + } + + get spieltag() { + if (!this.spieltage.size) + return -1; + return [...this.spieltage][0]; + } + + get name() { + return this.team_a.name + " :: " + this.team_b.name + ", Liga " + this.league; + } +}; + +function is_same(team_a, team_b) { + if (team_a.id == -1 || team_b.id == -1 || team_a.id != team_b.id) + return false; + return true; +} + +function is_same_ort(ort_a, ort_b) { + if (ort_a.id == -1 || ort_b.id == -1 || ort_a.id != ort_b.id) + return false; + return true; +} + +function createTextElement(name, text) { + let elem = document.createElement(name); + elem.appendChild(document.createTextNode(text)); + return elem; +} + +function appendChildList(elem, name, ...texts) { + for (const text of texts) + elem.appendChild(createTextElement(name, text)); +} + +function draw_table() { + /* Draw tables for each league */ + let anchor = document.getElementById("anchor"); + for (league of leagues) { + anchor.appendChild(createTextElement("h2", "Liga " + league.toString())); + + let table = document.createElement("table"); + let thead = document.createElement("thead"); + let tr = document.createElement("tr"); + appendChildList(tr, "th", "Heim", "Gäste", "Wochentag", "Tisch", "Spieltag"); + thead.appendChild(tr); + table.appendChild(thead); + + 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 } )) { + let tr = document.createElement("tr"); + appendChildList(tr, "td", paar.team_a.name, paar.team_b.name, paar.weekday, paar.ort.name, 1 + paar.spieltag); + + table.appendChild(tr); + } + anchor.appendChild(table); + } +} + +function fill_table(data) { + leagues = [...new Set(data.teams.map(team => team.liga))].sort(); + + /* Init some objects */ + for (ort of data.orte) + ort.pairings = new Array(); + + /* Count necessary spieltage */ + for (league of leagues) { + let pairings_count = data.teams.filter(team => team.liga == league).sort((a,b) => a.id > b.id).length; + if (pairings_count % 2) + pairings_count++; + max_spieltage = Math.max(2 * (pairings_count - 1), max_spieltage); + } + + /* Fill out complete table for all leagues */ + for (league of leagues) { + let league_teams = data.teams.filter(team => team.liga == league).sort((a,b) => a.id > b.id); + console.log( "liga " + league_teams.length ); + + for (team_a of league_teams) { + var game_count = 0; + for (team_b of league_teams) { + if (team_a == team_b) + continue; + + /* If Heimspiel-Team is smokers and the Gastteam is not, the alternative + location needs to be chosen */ + var ort = data.orte.find(ort => ort.id == team_a.heimort); + if (team_b.nichtraucher && ort.raucher) + ort = data.orte.find(ort => ort.id == team_a.ersatzort); + + pair = new paarung(team_a, team_b, league, ort); + tabelle.push(pair); + ort.pairings.push(pair); + game_count++; + } + /* Fill rest of this team's games with wildcard games */ + while (game_count < max_spieltage / 2) { + tabelle.push(new paarung(team_a, wildcard, league, nowhere)); + tabelle.push(new paarung(wildcard, team_a, league, nowhere)); + game_count++; + } + } + } + + /* Fill all leagues with uneven or fewer spieltage with wildcard games */ + for (league of leagues) { + + } + + /* Check if an ort is over-provisioned + for (ort of data.orte) { + for (weekday of [...new Set(ort.pairings.map(pair => pair.weekday))].sort()) { + var games_on_day = ort.pairings.filter(pair => pair.weekday = weekday); + if (games_on_day > max_spieltage) + alert("Ort " + ort.name + "over provisioned on weekday " + weekday_to_string(weekday)); + } + } */ +} + +function shuffle_array(array) { + return array.map(value => ({ value, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ value }) => value); +} + +function not_equal_function(s1, s2) { return s1 != s2; } + +function find_csp_solution() { + var all_spieltage = new Array(); + for (let i=0; i < max_spieltage; i++) + all_spieltage.push(i); + + for (league of leagues) { + var sub = tabelle.filter(paar => paar.league == league); + var candidate = {}, variables = {}, constraints = []; + for (outer of sub) { + variables[outer.id] = [...all_spieltage]; + for (inner of tabelle.filter(inner => + inner.id > outer.id && ( + is_same(outer.team_a, inner.team_a) || + is_same(outer.team_b, inner.team_a) || + is_same(outer.team_a, inner.team_b) || + is_same(outer.team_b, inner.team_b)))) + { + constraints.push([outer.id, inner.id, not_equal_function]); + } + } + + candidate.variables = variables; + candidate.constraints = constraints; + + var result = csp.solve(candidate); + console.log(result); + } +} + + +function find_table_configuration() { + /* Check if there is one possible configuration that fulfills the following criteria: + 1) each team must have 0 or 1 games on any given week + 2) each location+weekday combo must have 0 or 1 games on any given week + 3) each pairing needs a week and a location + */ + var candidates = tabelle; /* Take a copy of the array */ + + /* Add all possible spieltage to the list for + them to be later reduced */ + for (paar of candidates) { + paar.spieltage = new Set(); + for (let i=0; i < max_spieltage; i++) + paar.spieltage.add(i); + } + + /* TODO: Add more initial constraints */ + + while (candidates.length) { + /* First pick the pairings that have the least amount of spieltage to fit in */ + candidates = shuffle_array(candidates).sort((a, b) => a.spieltage.size <= b.spieltage.size); + + var looking_at = candidates.pop(); + + /* Pick random spieltag out of the possible ones */ + let spieltag = Array.from(looking_at.spieltage)[Math.floor(Math.random() * looking_at.spieltage.size)]; + + /* Now we have to remove all new conflicting dates from other unset pairings: */ + + /* Filter out all pairings on the same spieltag with the current team */ + for (paar of candidates.filter(paar => + is_same(paar.team_a, looking_at.team_a) || + is_same(paar.team_b, looking_at.team_a) || + is_same(paar.team_a, looking_at.team_b) || + is_same(paar.team_b, looking_at.team_b))) + { + paar.spieltage.delete(spieltag); + } + + /* Filter out all pairing on the same spieltag with the same ort and weekday + for (paar of candidates.filter(paar => + is_same_ort(paar.ort, looking_at.ort) && + paar.weekday == looking_at.weekday)) + { + if (paar.ort.id != -1 && looking_at.ort.id != -1) + paar.spieltage.delete(spieltag); + } + */ + + /* Filter out the reverse pairing until the second half of the season (Rueckrunde). + If it doesn't exist, we're probably already in Rueckrunde + var runden_start = Math.floor(max_spieltage * Math.floor(spieltag * 2 / max_spieltage) / 2); + for (paar of candidates.filter(paar => + is_same(paar.team_a, looking_at.team_b) || + is_same(paar.team_b, looking_at.team_a))) { + for (var exclude = 0; exclude < max_spieltage / 2; exclude++) + paar.spieltage.delete(runden_start + exclude); + } +*/ + /* Filter out home rounds for the home team on next spieltag and guest rounds for + guest team on next spieltag, except for end of Hinrunde + if (spieltag != max_spieltage / 2 - 1) + for (paar of candidates.filter(paar => + is_same(paar.team_a, looking_at.team_a) || + is_same(paar.team_b, looking_at.team_b))) + paar.spieltage.delete(spieltag + 1); +*/ + + for (paar of candidates) + if (paar.spieltage.size == 0) { + console.log(candidates.length); + return false; + } + + /* Now fix the date */ + looking_at.spieltage = new Set(); + looking_at.spieltage.add(spieltag); + } + return true; +} + +function init() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + fill_table(xhttp.response); + //find_table_configuration(); + find_csp_solution(); + draw_table(); + } + }; + xhttp.responseType = "json"; + xhttp.open("GET", API, true); + xhttp.send(); +} + +init(); -- cgit v1.2.3