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