summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFranklin Wei <me@fwei.tk>2019-04-23 14:30:02 -0400
committerFranklin Wei <me@fwei.tk>2019-04-23 14:30:02 -0400
commit13e4ccfa530903ffa1bab25dc4dd20390416c6e8 (patch)
tree8ab3d24617973eefb029a2ac80ccdce8845f1328
parentd5e24c18dd14855103c2914530dc9713e413ea69 (diff)
downloadtennisdraw-13e4ccfa530903ffa1bab25dc4dd20390416c6e8.zip
tennisdraw-13e4ccfa530903ffa1bab25dc4dd20390416c6e8.tar.gz
tennisdraw-13e4ccfa530903ffa1bab25dc4dd20390416c6e8.tar.bz2
tennisdraw-13e4ccfa530903ffa1bab25dc4dd20390416c6e8.tar.xz
Add server-side option
-rw-r--r--draw.js313
-rw-r--r--index.html1
-rw-r--r--server.js33
-rw-r--r--sort.js280
4 files changed, 345 insertions, 282 deletions
diff --git a/draw.js b/draw.js
index 9228eab..9fb97fd 100644
--- a/draw.js
+++ b/draw.js
@@ -190,60 +190,6 @@ function assign(i, player)
});
}
-function sameTeam(a, b)
-{
- return a.school == b.school;
-}
-
-function countSameTeam(half, player)
-{
- var n = 0;
- for(var i = 0; i < half.length; i++)
- if(sameTeam(half[i], player))
- n++;
- return n;
-}
-
-function countSameConference(half, player)
-{
- var n = 0;
- for(var i = 0; i < half.length; i++)
- if(half[i].conf == player.conf)
- n++;
- return n;
-}
-
-function inHalf(half, player)
-{
- for(var i = 0; i < half.length; i++)
- if(half[i].id == player.id)
- return true;
- return false;
-}
-
-function inHalves(halves, player)
-{
- return inHalf(halves[0], player) || inHalf(halves[1], player);
-}
-
-// returns 0 or 1 with a / (a + b) and b / (a + b) chance, respectively
-function randBucket(a, b)
-{
- if(a + b == 0)
- log("WARNING: randbucket with 0, 0");
- return (Math.random() < a / ( a + b )) ? 0 : 1;
-}
-
-function countEmptySlots(halves, halfsize)
-{
- return [halfsize - halves[0].length, halfsize - halves[1].length];
-}
-
-function countEmptyConfSlots(halves, player, halfsize)
-{
- return [halfsize / 2 - countSameConference(halves[0], player), halfsize / 2 - countSameConference(halves[1], player)];
-}
-
var havewarning;
function warning(divname, message)
@@ -261,89 +207,6 @@ function clearWarning(divname)
havewarning = false;
}
-// place player in a half based on these criteria (ranked by precedence):
-// 1. the half that is not full (if other is full)
-// 2. the half that has fewer teammates
-// 3. randomly, based on how many conference slots are available
-function placeHalf(halves, player, halfsize)
-{
- log("Placing player " + player.name + " in half...");
-
- // which half
- var idx;
- var emptySlots = countEmptySlots(halves, halfsize);
-
- // shouldn't be both full
- console.assert(emptySlots[0] + emptySlots[1] > 0);
-
- if(emptySlots[0] == 0 || emptySlots[1] == 0)
- {
- // only one open half, exit early
- idx = (emptySlots[0] == 0) ? 1 : 0;
- halves[idx].push(player);
- log(">one half full, defaulting to half " + idx);
-
- return true;
- }
-
- // max of 4 from same conference in each half
- var confSlots = countEmptyConfSlots(halves, player, halfsize);
-
- // warn user if exceeded
- if(confSlots[0] == 0 && confSlots[1] == 0)
- {
- warning("warn1", "There are too many players from the same conference to satisfy the different-conference rule. At least two players from the same conference will play each other in the first round.");
- }
-
- // place in half with fewer teammates, otherwise randomly based on conference slots
- var n = [countSameTeam(halves[0], player), countSameTeam(halves[1], player)];
-
- log(">teammate count: " + n);
-
- idx = (n[0] < n[1]) ? 0 : 1;
-
- // should we violate the conference slots requirement? would we
- // rather have extra teammates in one half than same-conference
- // players in the first round?
-
- if(n[0] == n[1])
- {
- // randomize based on slots (the lesser of conference or empty slots) in each half
- idx = randBucket(Math.max(0, Math.min(emptySlots[0], confSlots[0])), Math.max(0, Math.min(emptySlots[1], confSlots[1])));
- log(">it's a tossup: assigning to " + idx + " (open conference slots are " + confSlots[0] + ", " + confSlots[1] + ")");
- }
- else
- {
- log(">half " + idx + " has fewer teammates (open conference slots are " + confSlots[0] + ", " + confSlots[1] + ")");
- }
-
- halves[idx].push(player);
- return true;
-}
-
-function placeSeeds(halves, players, a, b)
-{
- // two possibilities: S1: 3->0, 4->1, or S2: 3->1, 4->0
- // evaluate each and assign it a score (sum of duplicates in each half)
- // then choose lowest-scoring configuration, if any (otherwise random)
- var s1 = countSameTeam(halves[0], players[a]) + countSameTeam(halves[1], players[b]);
- var s2 = countSameTeam(halves[1], players[a]) + countSameTeam(halves[0], players[b]);
-
- log("Placing top seeds...");
- log("S1 score: " + s1);
- log("S2 score: " + s2);
-
- // half index of player a
- var idx = (s1 < s2) ? 0 : 1;
- if(s1 == s2)
- idx = randBucket(1, 1);
-
- log("Choosing arrangement " + idx);
-
- halves[idx].push(players[a]);
- halves[+!idx].push(players[b]);
-}
-
// two Players
function compareTeam(a, b)
{
@@ -366,143 +229,6 @@ function validate(players)
}
}
-function test_bit(num, bit){
- return +((num>>bit) % 2 != 0);
-}
-
-function getSchool(player)
-{
- return Number(player.school);
-}
-
-function getConf(player)
-{
- return Number(player.conf);
-}
-
-// sorts array so that the most frequent objects are first (values are determined by getValue(obj))
-// ties are broken by getValue()'s numerical value
-// objects with frequency 1 are removed
-function sortByFreq(arr, getValue)
-{
- var freq = {};
- arr.forEach(function(obj) { freq[getValue(obj)] = 0; });
- arr.forEach(function(obj) { freq[getValue(obj)]++; });
- var sorted = arr.sort(function(a, b) { var diff = freq[getValue(b)] - freq[getValue(a)]; if(diff != 0) return diff; else return getValue(b) - getValue(a);});
- return sorted.filter(function(obj) { return freq[getValue(obj)] > 1; });
-}
-
-// sort into two halves of any even length
-function divide(start, players, n)
-{
- var halves = start;
-
- // if there are multiple teammates sharing a common school, process those first
- var bySchool = sortByFreq(players, getSchool);
- console.log(bySchool);
-
- bySchool.forEach(function(pl) {
- if(!inHalves(halves, pl))
- placeHalf(halves, pl, n / 2);
- });
-
- // then if there are multiple same-conference players, process those next
- var byConf = sortByFreq(players, getConf);
-
- byConf.forEach(function(pl) {
- if(!inHalves(halves, pl))
- placeHalf(halves, pl, n / 2);
- });
-
- // then assign the rest
- for(var i = 0; i < n; i++)
- {
- if(!inHalves(halves, players[i]))
- placeHalf(halves, players[i], n / 2);
- }
-
- return halves;
-}
-
-function performSort(players)
-{ // divide into halves
- log("Performing half-sort");
-
- var halves = [ [], [] ];
-
- // place 4 seeds
- halves[0].push(players[0]);
- halves[1].push(players[1]);
- placeSeeds(halves, players, 2, 3);
-
- halves = divide(halves, players, 16);
-
- log("Halves: " + halves);
-
- // halves are sorted. now divide each half further into quarters.
-
- // array of two quarters for each half
- var quarters = [ [ [], [] ], [ [], [] ] ];
-
- // separate seeds
- for(var i = 0; i < 2; i++)
- for(var j = 0; j < 2; j++)
- quarters[i][j].push(halves[i][j]);
-
- // sort quarters
- for(var i = 0; i < 2; i++)
- {
- quarters[i] = divide(quarters[i], halves[i], 8);
- }
-
- log("Quarters: " + quarters);
-
- // finally, select two from each quarter to make eighths
- var eighths = [ [ [ [], [] ], [ [], [] ] ], [ [ [], [] ], [ [], [] ] ] ];
-
- // place seeds
- for(var i = 0; i < 2; i++)
- for(var j = 0; j < 2; j++)
- eighths[i][j][0].push(quarters[i][j][0]);
-
- for(var i = 0; i < 2; i++)
- {
- for(var j = 0; j < 2; j++)
- {
- eighths[i][j] = divide(eighths[i][j], quarters[i][j], 4);
- }
- }
-
- log("Eighths: " + eighths);
-
- var sorted = [];
-
- for(var i = 0; i < 16; i+=1)
- {
- sorted[i] = eighths[test_bit(i, 3)][test_bit(i, 2)][test_bit(i, 1)][test_bit(i, 0)];
- }
-
- // map internal order -> final placement
- var mapping = [
- 1, 2,
- 3, 4,
- 5, 6,
- 7, 8,
- 16, 15,
- 14, 13,
- 12, 11,
- 10, 9
- ];
-
- var final_sort = new Array(16);
- for(var i = 0; i < 16; i++)
- {
- final_sort[mapping[i] - 1] = sorted[i];
- }
-
- return final_sort;
-}
-
function tryDraw()
{
var isValid = true;
@@ -530,6 +256,36 @@ function getBracketName()
return name;
}
+function handleResults(final_sort)
+{
+ for(var i = 0; i < 16; i++)
+ assign(i + 1, final_sort[i]);
+
+ name = getBracketName();
+ $("#bracketname").text(name);
+ $("#bracket").show();
+}
+
+// call server
+function doPerformSort(players, callback)
+{
+
+ var url = "http://localhost:8080";
+ $.ajax({
+ url:url,
+ type:"POST",
+ data:JSON.stringify(players),
+ contentType:"application/json; charset=utf-8",
+ dataType: "json",
+ success: function(data, status, xhr) { callback(JSON.parse(xhr.responseText)); }
+ });
+
+
+ // otherwise
+ //handleResults(performSort(players));
+}
+
+
function draw() {
logging = $("input[name='logging']").is(":checked");
@@ -544,14 +300,7 @@ function draw() {
validate(players);
- var final_sort = performSort(players);
-
- for(var i = 0; i < 16; i++)
- assign(i + 1, final_sort[i]);
-
- name = getBracketName();
- $("#bracketname").text(name);
- $("#bracket").show();
+ doPerformSort(players, handleResults);
}
/**
diff --git a/index.html b/index.html
index d37c00c..5d09758 100644
--- a/index.html
+++ b/index.html
@@ -178,6 +178,7 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="draw.js"></script>
+ <!--<script src="sort.js"></script>-->
<script src="https://cdn.metroui.org.ua/v4/js/metro.min.js"></script>
</body>
</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..84a6363
--- /dev/null
+++ b/server.js
@@ -0,0 +1,33 @@
+const express = require('express');
+const sort = require('./sort.js');
+const app = express();
+const port = 8080;
+
+app.get('/', (req, res) => res.send(''))
+
+// Parse URL-encoded bodies (as sent by HTML forms)
+app.use(express.urlencoded());
+
+// Parse JSON bodies (as sent by API clients)
+app.use(express.json());
+
+app.use(function(req, res, next) {
+ res.header("Access-Control-Allow-Origin", "*");
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ next();
+});
+
+// Access the parse results as request.body
+app.post('/', function(request, response){
+ var players = request.body;
+ console.log(players);
+
+ var result = sort.performSort(players);
+ response.header('Access-Control-Allow-Origin', '*');
+ response.write(JSON.stringify(result));
+
+ console.log(result);
+ response.end();
+});
+
+app.listen(port, () => console.log(`Example app listening on port ${port}!`))
diff --git a/sort.js b/sort.js
new file mode 100644
index 0000000..0756452
--- /dev/null
+++ b/sort.js
@@ -0,0 +1,280 @@
+function log(msg)
+{
+ // do nothing
+}
+
+function sameTeam(a, b)
+{
+ return a.school == b.school;
+}
+
+function countSameTeam(half, player)
+{
+ var n = 0;
+ for(var i = 0; i < half.length; i++)
+ if(sameTeam(half[i], player))
+ n++;
+ return n;
+}
+
+function countSameConference(half, player)
+{
+ var n = 0;
+ for(var i = 0; i < half.length; i++)
+ if(half[i].conf == player.conf)
+ n++;
+ return n;
+}
+
+function inHalf(half, player)
+{
+ for(var i = 0; i < half.length; i++)
+ if(half[i].id == player.id)
+ return true;
+ return false;
+}
+
+function inHalves(halves, player)
+{
+ return inHalf(halves[0], player) || inHalf(halves[1], player);
+}
+
+// returns 0 or 1 with a / (a + b) and b / (a + b) chance, respectively
+function randBucket(a, b)
+{
+ if(a + b == 0)
+ log("WARNING: randbucket with 0, 0");
+ return (Math.random() < a / ( a + b )) ? 0 : 1;
+}
+
+function countEmptySlots(halves, halfsize)
+{
+ return [halfsize - halves[0].length, halfsize - halves[1].length];
+}
+
+function countEmptyConfSlots(halves, player, halfsize)
+{
+ return [halfsize / 2 - countSameConference(halves[0], player), halfsize / 2 - countSameConference(halves[1], player)];
+}
+
+// place player in a half based on these criteria (ranked by precedence):
+// 1. the half that is not full (if other is full)
+// 2. the half that has fewer teammates
+// 3. randomly, based on how many conference slots are available
+function placeHalf(halves, player, halfsize)
+{
+ log("Placing player " + player.name + " in half...");
+
+ // which half
+ var idx;
+ var emptySlots = countEmptySlots(halves, halfsize);
+
+ // shouldn't be both full
+ console.assert(emptySlots[0] + emptySlots[1] > 0);
+
+ if(emptySlots[0] == 0 || emptySlots[1] == 0)
+ {
+ // only one open half, exit early
+ idx = (emptySlots[0] == 0) ? 1 : 0;
+ halves[idx].push(player);
+ log(">one half full, defaulting to half " + idx);
+
+ return true;
+ }
+
+ // max of 4 from same conference in each half
+ var confSlots = countEmptyConfSlots(halves, player, halfsize);
+
+ // warn user if exceeded
+ if(confSlots[0] == 0 && confSlots[1] == 0)
+ {
+ log("There are too many players from the same conference to satisfy the different-conference rule. At least two players from the same conference will play each other in the first round.");
+ }
+
+ // place in half with fewer teammates, otherwise randomly based on conference slots
+ var n = [countSameTeam(halves[0], player), countSameTeam(halves[1], player)];
+
+ log(">teammate count: " + n);
+
+ idx = (n[0] < n[1]) ? 0 : 1;
+
+ // should we violate the conference slots requirement? would we
+ // rather have extra teammates in one half than same-conference
+ // players in the first round?
+
+ if(n[0] == n[1])
+ {
+ // randomize based on slots (the lesser of conference or empty slots) in each half
+ idx = randBucket(Math.max(0, Math.min(emptySlots[0], confSlots[0])), Math.max(0, Math.min(emptySlots[1], confSlots[1])));
+ log(">it's a tossup: assigning to " + idx + " (open conference slots are " + confSlots[0] + ", " + confSlots[1] + ")");
+ }
+ else
+ {
+ log(">half " + idx + " has fewer teammates (open conference slots are " + confSlots[0] + ", " + confSlots[1] + ")");
+ }
+
+ halves[idx].push(player);
+ return true;
+}
+
+function placeSeeds(halves, players, a, b)
+{
+ // two possibilities: S1: 3->0, 4->1, or S2: 3->1, 4->0
+ // evaluate each and assign it a score (sum of duplicates in each half)
+ // then choose lowest-scoring configuration, if any (otherwise random)
+ var s1 = countSameTeam(halves[0], players[a]) + countSameTeam(halves[1], players[b]);
+ var s2 = countSameTeam(halves[1], players[a]) + countSameTeam(halves[0], players[b]);
+
+ log("Placing top seeds...");
+ log("S1 score: " + s1);
+ log("S2 score: " + s2);
+
+ // half index of player a
+ var idx = (s1 < s2) ? 0 : 1;
+ if(s1 == s2)
+ idx = randBucket(1, 1);
+
+ log("Choosing arrangement " + idx);
+
+ halves[idx].push(players[a]);
+ halves[+!idx].push(players[b]);
+}
+
+function getSchool(player)
+{
+ return Number(player.school);
+}
+
+function getConf(player)
+{
+ return Number(player.conf);
+}
+
+// sorts array so that the most frequent objects are first (values are determined by getValue(obj))
+// ties are broken by getValue()'s numerical value
+// objects with frequency 1 are removed
+function sortByFreq(arr, getValue)
+{
+ var freq = {};
+ arr.forEach(function(obj) { freq[getValue(obj)] = 0; });
+ arr.forEach(function(obj) { freq[getValue(obj)]++; });
+ var sorted = arr.sort(function(a, b) { var diff = freq[getValue(b)] - freq[getValue(a)]; if(diff != 0) return diff; else return getValue(b) - getValue(a);});
+ return sorted.filter(function(obj) { return freq[getValue(obj)] > 1; });
+}
+
+function test_bit(num, bit){
+ return +((num>>bit) % 2 != 0);
+}
+
+// sort into two halves of any even length
+function divide(start, players, n)
+{
+ var halves = start;
+
+ // if there are multiple teammates sharing a common school, process those first
+ var bySchool = sortByFreq(players, getSchool);
+
+ bySchool.forEach(function(pl) {
+ if(!inHalves(halves, pl))
+ placeHalf(halves, pl, n / 2);
+ });
+
+ // then if there are multiple same-conference players, process those next
+ var byConf = sortByFreq(players, getConf);
+
+ byConf.forEach(function(pl) {
+ if(!inHalves(halves, pl))
+ placeHalf(halves, pl, n / 2);
+ });
+
+ // then assign the rest
+ for(var i = 0; i < n; i++)
+ {
+ if(!inHalves(halves, players[i]))
+ placeHalf(halves, players[i], n / 2);
+ }
+
+ return halves;
+}
+
+function performSort(players)
+{ // divide into halves
+ log("Performing half-sort");
+
+ var halves = [ [], [] ];
+
+ // place 4 seeds
+ halves[0].push(players[0]);
+ halves[1].push(players[1]);
+ placeSeeds(halves, players, 2, 3);
+
+ halves = divide(halves, players, 16);
+
+ log("Halves: " + halves);
+
+ // halves are sorted. now divide each half further into quarters.
+
+ // array of two quarters for each half
+ var quarters = [ [ [], [] ], [ [], [] ] ];
+
+ // separate seeds
+ for(var i = 0; i < 2; i++)
+ for(var j = 0; j < 2; j++)
+ quarters[i][j].push(halves[i][j]);
+
+ // sort quarters
+ for(var i = 0; i < 2; i++)
+ {
+ quarters[i] = divide(quarters[i], halves[i], 8);
+ }
+
+ log("Quarters: " + quarters);
+
+ // finally, select two from each quarter to make eighths
+ var eighths = [ [ [ [], [] ], [ [], [] ] ], [ [ [], [] ], [ [], [] ] ] ];
+
+ // place seeds
+ for(var i = 0; i < 2; i++)
+ for(var j = 0; j < 2; j++)
+ eighths[i][j][0].push(quarters[i][j][0]);
+
+ for(var i = 0; i < 2; i++)
+ {
+ for(var j = 0; j < 2; j++)
+ {
+ eighths[i][j] = divide(eighths[i][j], quarters[i][j], 4);
+ }
+ }
+
+ log("Eighths: " + eighths);
+
+ var sorted = [];
+
+ for(var i = 0; i < 16; i+=1)
+ {
+ sorted[i] = eighths[test_bit(i, 3)][test_bit(i, 2)][test_bit(i, 1)][test_bit(i, 0)];
+ }
+
+ // map internal order -> final placement
+ var mapping = [
+ 1, 2,
+ 3, 4,
+ 5, 6,
+ 7, 8,
+ 16, 15,
+ 14, 13,
+ 12, 11,
+ 10, 9
+ ];
+
+ var final_sort = new Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ final_sort[mapping[i] - 1] = sorted[i];
+ }
+
+ return final_sort;
+}
+
+if(typeof window === 'undefined')
+ exports.performSort = performSort;