With the javascript and html file this time

This commit is contained in:
Ken Knudsen 2021-08-23 20:29:53 +00:00
parent 206499c4f5
commit 36e0ba1b8b
2 changed files with 482 additions and 54 deletions

View File

@ -2,15 +2,38 @@
<html>
<head>
<title>Scoreboard</title>
<link rel="stylesheet" href="basic.css">
<link rel="stylesheet" href="scoreboard-base.css">
<link rel="stylesheet" href="gibson.css">
<meta name="viewport" content="width=device-width">
</head>
<body class="wide">
<h4 id="location"></h4>
<ul id="settings">
<em class="btn">Settings</em>
<li>
<ul>Ranking Format
<li><input type="radio" id="rankingStandard" name="rankingPerspective" value="standard" checked /> <label for="rankingStandard">Standard</label></li>
<li><input type="radio" id="rankingCategory" name="rankingPerspective" value="category" /> <label for="rankingCategory">Category</label></li>
<li><input type="radio" id="rankingTrack" name="rankingPerspective" value="track"/> <label for="rankingTrack">Track (<em>placeholder</em>)</label></li>
</ul>
<ul>Features
<li><input type="checkbox" id="mothLeaderIcons" name="mothLeaderIcons" value="true" /> <label for="mothLeaderIcons">Show Leader icons</label></li>
<li><input type="checkbox" id="mothScreenSaver" name="mothScreenSaver" value="true"/> <label for="mothScreenSaver">Moth ScreenSaver</label></li>
<ul style="margin-left: 2em;">
<li><input type="radio" id="mothScreenSaverBgNone" name="mothScreenSaverBg" value="transparent" /> <label for="mothScreenSaverBgNone">Transparent</label></li>
<li><input type="radio" id="mothScreenSaverBgText" name="mothScreenSaverBg" value="svg" checked /> <label for="mothScreenSaverBgText">SVG/Text</label></li>
<li><input type="radio" id="mothScreenSaverBgWindow" name="mothScreenSaverBg" value="window" /> <label for="mothScreenSaverBgWindow">Window</label></li>
<li><input type="radio" id="mothScreenSaverBgCombined" name="mothScreenSaverBg" value="combined" /> <label for="mothScreenSaverBgCombined">Combined</label></li>
</ul>
</ul>
</li>
</ul>
<section class="rotate">
<div id="chart"></div>
<div id="rankings"></div>
</section>
<nav>
<ul>
<li><a href="index.html">Puzzles</a></li>

View File

@ -1,8 +1,34 @@
// jshint asi:true
function scoreboardInit() {
var MOTH_RANKING_STANDARD=0;
var MOTH_RANKING_CATEGORY=1;
var MOTH_RANKING_TRACK=2;
chartColors = [
// Comparison functions
var MOTH_COMP_TEAMOVERALL=function (a, b) {
return a.overallScore - b.overallScore;
}
var MOTH_COMP_POINTSLOGTIME=function(a, b) {
return a[0] - b[0];
}
var MOTH_COMP_SCORE=function(a, b) {
return a - b;
}
var teamNames={};
var pointsLog={};
function scoreboardInit() {
// Visual flare
var cortex={
ranking: MOTH_RANKING_STANDARD,
sorting: MOTH_COMP_TEAMOVERALL,
screenSaverBg: "svg",
};
var chartColors = [
"rgb(255, 99, 132)",
"rgb(255, 159, 64)",
"rgb(255, 205, 86)",
@ -12,6 +38,18 @@ function scoreboardInit() {
"rgb(201, 203, 207)"
]
// Placeholder for Track mappings
trackMap = {
"Operational-Technology" : "ot",
"Safe_Malware" : "malware",
"linux_memory_intro" : "forensics",
"js" : "netarch",
"sequence" : "netarch",
"networking" : "entry-point",
"codebreaking" : "incident-coordination",
"nocode" : "entry-point",
}
function update(state) {
window.state = state
@ -19,9 +57,8 @@ function scoreboardInit() {
rotate.appendChild(rotate.firstElementChild)
}
let element = document.getElementById("rankings")
let teamNames = state.TeamNames
let pointsLog = state.PointsLog
teamNames = state.TeamNames
pointsLog = state.PointsLog
// Every machine that's displaying the scoreboard helpfully stores the last 20 values of
// points.json for us, in case of catastrophe. Thanks, y'all!
@ -35,61 +72,65 @@ function scoreboardInit() {
stateHistory.push(state)
localStorage.setItem("stateHistory", JSON.stringify(stateHistory))
draw();
}
function draw() {
let teams = {}
let categories = {} // map[string][team]int
let highestCategoryScore = {} // map[string]int
let uiRanking=cortex.ranking.valueOf()
let element = document.getElementById("rankings")
// Initialize data structures
for (let teamId in teamNames) {
teams[teamId] = {
categoryScore: {}, // map[string]int
trackScore: {}, // map[string]int
overallScore: 0, // int
historyLine: [], // []{x: int, y: int}
history: [], // []{t: timestamp, c: category, s: int}
name: teamNames[teamId],
id: teamId
}
}
// Dole out points
for (let entry of pointsLog) {
let timestamp = entry[0]
let teamId = entry[1]
let category = entry[2]
let points = entry[3]
let team = teams[teamId]
let score = team.categoryScore[category] || 0
score += points
team.categoryScore[category] = score
let highest = highestCategoryScore[category] || 0
if (score > highest) {
highestCategoryScore[category] = score
}
}
for (let teamId in teamNames) {
teams[teamId].categoryScore = {}
}
pointsLog.sort(MOTH_COMP_POINTSLOGTIME)
for (let entry of pointsLog) {
let timestamp = entry[0]
let teamId = entry[1]
let category = entry[2]
let points = entry[3]
let points = parseInt(entry[3])
let team = teams[teamId]
let score = team.categoryScore[category] || 0
let trackScore = team.trackScore[trackMap[category]] || 0
score += points
trackScore += points
team.categoryScore[category] = score
let overall = 0
for (let cat in team.categoryScore) {
overall += team.categoryScore[cat] / highestCategoryScore[cat]
team.history.push({t: new Date(timestamp * 1000), cat: category, score: points})
if (!categories[category]) {
categories[category]={}
}
team.historyLine.push({t: new Date(timestamp * 1000), y: overall})
categories[category][teamId]=score
}
// Search Team score aggregates for highest scores and key markers
for (let cat in categories) {
let scores=Object.values(categories[cat])
scores.sort(MOTH_COMP_SCORE)
scores.reverse();
let highest=scores[0]
highestCategoryScore[cat]=highest.valueOf()
}
// Compute overall scores based on current highest
@ -99,19 +140,24 @@ function scoreboardInit() {
for (let cat in team.categoryScore) {
team.overallScore += team.categoryScore[cat] / highestCategoryScore[cat]
}
// HistoryLine
let overall = 0
for (let history in team.history) {
let entry=team.history[history]
overall+=entry.score/highestCategoryScore[entry.cat]
team.historyLine.push({t: entry.t, y: overall.toFixed(2)})
}
// Sort by team score
function teamCompare(a, b) {
return a.overallScore - b.overallScore
}
// Figure out how to order each team on the scoreboard
let winners = []
for (let teamId in teams) {
winners.push(teams[teamId])
}
winners.sort(teamCompare)
winners.sort(cortex.sorting)
winners.reverse()
// Let's make some better names for things we've computed
@ -121,26 +167,67 @@ function scoreboardInit() {
// Clear out the element we're about to populate
Array.from(element.childNodes).map(e => e.remove())
let maxWidth = 100 / winningScore
let maxWidth = (100 / winningScore)
let avgWidth = (100 / numCategories)
// Pre-load heading
let headingRow=document.createElement("div")
headingRow.id="rankHeading";
let headingRowPoints=document.createElement("span")
headingRowPoints.classList.add("teampoints")
headingRowPoints.classList.add("inv")
let headingNcat=0
for (let category in highestCategoryScore) {
let bar=document.createElement("span")
bar.title=category
bar.style.width=avgWidth +"%"
bar.classList.add("cat" +headingNcat)
bar.textContent=category
bar.dataset.category=category;
bar.onclick=sortByCategory;
headingRowPoints.appendChild(bar);
headingNcat+=1
}
headingRow.appendChild(headingRowPoints);
element.appendChild(headingRow)
for (let team of winners) {
let row = document.createElement("div")
row.classList.add("team");
row.dataset.overallScore=team.overallScore.toFixed(2);
let ncat = 0
let teamPoints=document.createElement("span")
teamPoints.classList.add("teampoints")
let leader=[];
for (let category in highestCategoryScore) {
let catHigh = highestCategoryScore[category]
let catTeam = team.categoryScore[category] || 0
let catPct = catTeam / catHigh
let width = maxWidth * catPct
let width = (maxWidth * catPct)
let catWidth = (avgWidth * catPct)
let bar = document.createElement("span")
bar.classList.add("category")
bar.classList.add("cat" + ncat)
bar.style.width = width + "%"
bar.textContent = category + ": " + catTeam
bar.title = bar.textContent
bar.dataset.standardWidth=width;
bar.dataset.categoryWidth=catWidth;
bar.dataset.category=category;
bar.dataset.points=catTeam;
bar.dataset.categoryMargin=(avgWidth - catWidth)
bar.title = bar.dataset.category + ": " + bar.dataset.points
if ((catTeam == catHigh) && (trackMap[category])){
leader.push(trackMap[category])
}
displayMothRanking(uiRanking, bar);
teamPoints.appendChild(bar)
ncat += 1
@ -151,6 +238,17 @@ function scoreboardInit() {
let te = document.createElement("span")
te.classList.add("teamname")
te.textContent = team.name
for (let track in leader) {
te.classList.add("leader");
let img=document.createElement("img");
img.classList.add("icon");
img.classList.add("track-"+leader[track]);
te.prepend(img);
}
row.appendChild(te)
element.appendChild(row)
@ -236,22 +334,329 @@ function scoreboardInit() {
})
}
let imgRL=null;
let imgLR=null;
let canvas=null;
function setScreenSaver() {
let uiScreenSaver=document.querySelector("#mothScreenSaver");
if (uiScreenSaver.checked) {
if (canvas == null) {
canvas=document.createElement("picture");
canvas.id="gibson"
}
document.querySelector("body").appendChild(canvas);
if (imgRL == null) {
imgRL=document.createElement("img");
imgRL.classList.add("moth");
imgRL.src="luna-moth.svg";
imgRL.style.animation="slideRL 4s linear infinite";
imgRL.dataset.direction="RL";
imgRL.addEventListener("animationiteration", lunaRLIterListener, false);
}
imgRL.style.top="4em";
if (imgLR == null) {
imgLR=document.createElement("img");
imgLR.classList.add("moth");
imgLR.src="luna-moth.svg";
imgLR.style.animation="slideLR 4s linear infinite";
imgLR.dataset.direction="LR";
imgLR.addEventListener("animationiteration", lunaLRIterListener, false);
}
imgLR.style.top=0;
canvas.appendChild(imgLR);
canvas.appendChild(imgRL);
setScreenSaverBg();
setTimeout(lunaShadow, Math.random()*100+400);
} else {
canvas=document.querySelector("#gibson");
if (canvas) {
canvas.remove();
}
canvas=null;
imgRL=null;
imgLR=null;
}
}
function setScreenSaverBg() {
let options=["svg", "window", "combined"];
document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) {
if (item.checked === true) {
cortex.screenSaverBg=item.value;
}
});
if (canvas !== null) {
for (let i=0; i<options.length; i++) {
if (options[i] != cortex.screenSaverBg) {
canvas.classList.remove(options[i]);
}
canvas.classList.add(cortex.screenSaverBg);
}
}
}
function animateScreenSaver() {
let img=document.querySelector("#screensaverImg");
if (img == null) {
return false;
}
let coord=img.getBoundingClientRect();
img.style.transitionDuration="0s"
img.style.transform="translate("+ Math.max((-1*coord.width), (-1*(coord.width+coord.x))) + "px, " + (coord.y + coord.height) + "px) rotate(135deg)";
setTimeout(function() {
img.style.transitionDuration="5s"
img.style.transform="translate(100vw, " + (coord.y + coord.height) + "px) rotate(135deg)";
}, 100);
setTimeout(animateScreenSaver, 5200);
return true;
}
function setShowLeaderIcons() {
let uiShowLeaderIcons=document.querySelector("#mothLeaderIcons");
let body=document.querySelector("body");
(uiShowLeaderIcons.checked)? body.classList.add("fun") : body.classList.remove("fun");
}
function setRankingTrack() {
let uiRankingType=document.querySelector("input[name=rankingPerspective]:checked");
switch (uiRankingType.value) {
case "track":
cortex.ranking=MOTH_RANKING_TRACK;
break;
case "category":
cortex.ranking=MOTH_RANKING_CATEGORY;
break;
case "standard":
default:
cortex.ranking=MOTH_RANKING_STANDARD;
}
let uiRanking=cortex.ranking.valueOf();
document.querySelectorAll(".category").forEach(function(item) {
displayMothRanking(uiRanking, item)
});
switch (uiRanking) {
case MOTH_RANKING_CATEGORY:
case MOTH_RANKING_TRACK:
document.querySelector("#rankings").classList.add("track");
break;
case MOTH_RANKING_STANDARD:
default:
cortex.sorting = MOTH_COMP_TEAMOVERALL;
setTimeout(sortByCategory, 1000);
document.querySelector("#rankings").classList.remove("track");
}
}
function displayMothRanking(rankingStyle, obj) {
switch (rankingStyle) {
case MOTH_RANKING_CATEGORY:
case MOTH_RANKING_TRACK:
obj.style.width=obj.dataset.categoryWidth + "%";
obj.style.marginRight=obj.dataset.categoryMargin + "%";
obj.textContent = obj.dataset.points;
break;
case MOTH_RANKING_STANDARD:
default:
obj.style.width=obj.dataset.standardWidth + "%";
obj.style.marginRight=0+"%";
obj.textContent = obj.dataset.category + ": " + obj.dataset.points
}
}
// Sort winners by category score
function sortByCategory() {
let cat=(this.dataset)? this.dataset.category || MOTH_COMP_TEAMOVERALL : MOTH_COMP_TEAMOVERALL;
let teamOrder=Array.from(document.querySelectorAll("#rankings div.team"));
let rankings=document.querySelector("#rankings");
// Grab current screen positions of objects
let coords=[]
for (let i=0; i<teamOrder.length; i++) {
coords.push({ x: teamOrder[i].offsetLeft, y: teamOrder[i].offsetTop });
}
// console.log(teamOrder[0].offsetTop);
// teamOrder.forEach(function(item) {
// item.style.top=item.offsetTop +"px";
// item.style.left=item.offsetLeft +"px";
// });
if (cat == MOTH_COMP_TEAMOVERALL) {
teamOrder.sort(function(a, b) {
return b.dataset.overallScore - a.dataset.overallScore;
});
} else {
teamOrder.sort(function(a, b) {
let aa=a.querySelector("span[data-category=" + cat + "]") || { dataset : {points: 0}};
let bb=b.querySelector("span[data-category=" + cat + "]") || { dataset : {points: 0}};
return bb.dataset.points - aa.dataset.points;
});
}
// Move elements without rearranging the DOM
// Seems to be a couple ways to do this.
// 1. appendChild in DOM will move the element without transition
// 2. translate to move the elements where they should be with transition
teamOrder.forEach(function(item, i) {
item.style.transform="translate(" + (coords[i]["x"] - item.offsetLeft) + "px, " + (coords[i]["y"] - item.offsetTop) +"px)"
// rankings.appendChild(item);
// item.style.transform="translate(" + (item-offsetLeft - coords[i]["x"]) + "px, " + (item.offsetTop - coords[i]["y"]) +"px)"
});
}
/* Luna Moth ScreenSaver */
function lunaRLIterListener(event) {
let coord=imgRL.getBoundingClientRect();
if ((coord.y + coord.height) > canvas.clientHeight) {
// Repeat in infinite loop
// imgRL.style.top="4em";
// Uncomment to run through screen once
imgRL.style.animationPlayState="paused";
} else {
imgRL.style.top=(coord.y + (2*coord.height))+"px";
}
}
function lunaLRIterListener(event) {
let coord=imgLR.getBoundingClientRect();
if ((coord.y + coord.height) > canvas.clientHeight) {
// Repeat in infinite loop
// imgLR.style.top="0px";
// Uncomment to run through screen once
imgLR.style.animationPlayState="paused";
} else {
imgLR.style.top=(coord.y + (2*coord.height))+"px";
}
}
function lunaShadow() {
// let coordCanvas=canvas.getBoundingClientRect();
let targets=[imgRL, imgLR];
targets.forEach(function(item) {
if (item == null) {
return false;
}
let coord=item.getBoundingClientRect();
let div=document.createElement("div");
div.className="shadow "+item.dataset.direction;
div.style.top=item.style.top;
div.style.left=coord.left +"px";
canvas.appendChild(div);
});
// Only run through screen once
if ((imgRL != null) && (imgRL.style.animationPlayState != "paused")) {
setTimeout(lunaShadow, Math.random()*400+200);
}
}
function init() {
let base = window.location.href.replace("scoreboard.html", "")
let location = document.querySelector("#location")
let params = new URLSearchParams(document.location.search.substring(1));
if (location) {
location.textContent = base
}
// Grab initial settings and set event handlers
document.querySelectorAll("input[name=rankingPerspective]").forEach(function(item) {
if (item.value == params.get(item.name)) {
item.checked=true;
}
item.onchange=setRankingTrack;
});;
// Leader Icons
let leaderIcons=document.querySelector("#mothLeaderIcons");
if (leaderIcons) {
leaderIcons.checked=(params.get(leaderIcons.name) == leaderIcons.value);
leaderIcons.onchange=setShowLeaderIcons;
}
// Screensaver
let screenSaver=document.querySelector("#mothScreenSaver");
if (screenSaver) {
screenSaver.checked=(params.get(screenSaver.name) == screenSaver.value);
screenSaver.onchange=setScreenSaver;
}
document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) {
if (item.value == params.get(item.name)) {
item.checked=true;
}
item.onchange=setScreenSaverBg;;
});
setRankingTrack();
setShowLeaderIcons();
setScreenSaver();
setInterval(refresh, 60000)
refresh()
}
init()
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scoreboardInit)
} else {
scoreboardInit()
}