diff --git a/python/static/controller.css b/python/static/controller.css deleted file mode 100644 index 0362408..0000000 --- a/python/static/controller.css +++ /dev/null @@ -1,34 +0,0 @@ -body { - background: #222; - color: #888; -} - -.center { - text-align: center; - width: 100%; -} - -#display { - display: inline; -} - -#modules div { - display: inline-block; - color: #111; - font-size: 14pt; - font-family: monospace; - border-radius: 25%; - padding: 20px; -} - -.solved { - background-color: green; -} - -.needy { - background-color: grey; -} - -.unsolved { - background-color: yellow; -} diff --git a/python/static/controller.html b/python/static/controller.html index 1df0c7a..348b61f 100644 --- a/python/static/controller.html +++ b/python/static/controller.html @@ -1,23 +1,48 @@ - - + - - - OBUS controller - - + + + - -

Game state:

+ OBUS controller + - - - + + +
+ +
+
Game state:
+
-
- Timer should be here -
- -
- + +
+ + +
+
+ + +
+ +
+ + Timer should be here + +
+ + +
+
Modules
+ +
+
+
+ + + + + diff --git a/python/static/controller.js b/python/static/controller.js deleted file mode 100644 index 6c8a6f3..0000000 --- a/python/static/controller.js +++ /dev/null @@ -1,108 +0,0 @@ -let estimated_timeout_date; -let display; -let gamestate; -let last_strike_amount = 0; -let strike_audio = new Audio('/static/strike.mp3'); -strike_audio.preload = 'auto'; -let last_puzzle_state = [] - -function startbutton() { - fetch('/start'); -} - -function restartbutton() { - fetch('/restart'); -} - -function updateDisplay() { - if (gamestate != 'GAME' || !estimated_timeout_date) { - return; - } - setTimeleft((estimated_timeout_date - (new Date()))/1000); -} - -function updateModuleState(data) { - if (data.gamestate == 'INACTIVE' || data.gamestate == 'INFO') { - document.getElementById("modules").innerHTML = ''; - } - else { - let newmodulestate = []; - for (const puzzlestate of data.puzzles) { - let modulediv = document.createElement('div'); - modulediv.textContent = puzzlestate.address; - if (puzzlestate.solved === true) { - modulediv.className = "solved"; - } else if (puzzlestate.solved === false) { - modulediv.className = "unsolved"; - } else if (puzzlestate.solved === null) { - modulediv.className = "needy"; - } - newmodulestate.push(modulediv); - } - document.getElementById("modules").replaceChildren(...newmodulestate); - } -} - -function setTimeleft(timeleft) { - if (timeleft <= 0) { - display.setValue('00:00.0'); - return - } - let integral = Math.floor(timeleft); - let fractional = timeleft - integral; - let minutes = Math.floor(integral / 60); - let seconds = integral % 60; - display.setValue(String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + '.' + String(Math.floor(fractional * 10))); -} - -function state_update() { - // TODO automatically timeout this request after the update interval - fetch('/status.json') - .then( - function(response) { - if (response.status !== 200) { - console.log('FAIL: ' + response.status); - return; - } - response.json().then(function(data) { - gamestate = data.gamestate; - document.getElementById('gamestate').innerHTML = gamestate; - if (gamestate != 'GAME') { - last_strike_amount = 0; - } - else if (gamestate == 'GAME') { - // TODO maybe smooth this with the previous value of estimated_timeout_date? - let new_estimate = new Date(); - new_estimate.setTime(new_estimate.getTime() + data.timeleft*1000); - estimated_timeout_date = new_estimate; - let total_strikes = data.puzzles.map(x => x.strikes).reduce((a, b) => a + b, 0); - if (last_strike_amount < total_strikes) { - strike_audio.load(); - strike_audio.play(); - } - last_strike_amount = total_strikes; - } - updateModuleState(data); - }); - } - ) -} - -window.onload = function() { - setInterval(state_update, 500); - display = new SegmentDisplay("display"); - display.pattern = "##:##.#"; - display.displayAngle = 0; - display.digitHeight = 100; - display.digitWidth = display.digitHeight / 2; - display.digitDistance = display.digitHeight / 10; - display.segmentWidth = display.digitHeight / 10; - display.segmentDistance = display.digitHeight / 100; - display.segmentCount = 7; - display.cornerType = 1; - display.colorOn = "#24dd22"; - display.colorOff = "#1b4105"; - display.draw(); - display.draw(); - setInterval(updateDisplay, 100); -}; diff --git a/python/static/css/controller.css b/python/static/css/controller.css new file mode 100644 index 0000000..f01a61a --- /dev/null +++ b/python/static/css/controller.css @@ -0,0 +1,108 @@ +@font-face { + font-family: "digital"; + src: url("/static/fonts/digital-webfont.woff"); + font-weight: normal; + font-style: normal; +} + +* { + box-sizing: border-box; +} + +body { + background: #222; + color: white; + font-family: "digital", sans-serif; + font-size: 32px; + padding: 0; + margin: 0; +} + +button { + display: inline-block; + color: #333; + background: #ddd; + padding: 0.5rem 1rem; + + font-family: inherit; + font-size: 1.5rem; + + border-radius: 3px; + box-shadow: inset 2px 2px #fff, inset -2px -2px #bbb, + 4px 4px rgba(0, 0, 0, 0.2); + + cursor: pointer; +} + +button:disabled { + cursor: default; + color: #999999 !important; +} + +#buttonStart { + color: green; +} + +.hud { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 2rem; +} + +.hud-state { + font-size: #bbbbbb; +} + +.content { + padding: 0 2rem; +} + +.box { + background: #000; + border: 5px solid #4d4d4d; + box-shadow: inset 0 0 0 10px #d6d6d6, 0 10px 0 -5px rgba(0, 0, 0, 0.2); + padding: 16px; +} + +.box-header { + font-size: 3rem; + margin-bottom: 16px; +} + +.center { + text-align: center; +} + +#display { + display: inline; + padding: 1rem 5rem; +} + +#modules > div { + display: inline-block; + color: #333; + background: #ddd; + padding: 0.5rem 1rem; + + font-family: inherit; + font-size: 3rem; + + border-radius: 3px; + box-shadow: inset 2px 2px #fff, inset -2px -2px #bbb, + 4px 4px rgba(0, 0, 0, 0.2); + margin: 8px; +} + +.solved { + background-color: green !important; + color: white !important; +} + +.needy { + background-color: grey !important; +} + +.unsolved { + background-color: yellow !important; +} diff --git a/python/static/debugger.html b/python/static/debugger.html index 0f7f3bd..beffa4c 100644 --- a/python/static/debugger.html +++ b/python/static/debugger.html @@ -1,118 +1,140 @@ - + - - - CAN debugger - - + .colorblock { + height: 20px; + width: 20px; + margin: 3.4px; + margin-right: 8.4px; + display: inline-block; + background-color: lime; + vertical-align: middle; + } + + - - - + + + - - + + - - + + - - - - - - - - - -
Human-readable typeSender IDParsed payloadTimeRaw MessageRaw ID
- - + + + + + + + + + +
Human-readable typeSender IDParsed payloadTimeRaw MessageRaw ID
+ + diff --git a/python/static/fonts/digital-webfont.woff b/python/static/fonts/digital-webfont.woff new file mode 100644 index 0000000..c7fd76c Binary files /dev/null and b/python/static/fonts/digital-webfont.woff differ diff --git a/python/static/js/controller.js b/python/static/js/controller.js new file mode 100644 index 0000000..48f9d8f --- /dev/null +++ b/python/static/js/controller.js @@ -0,0 +1,211 @@ +/** + * Soundeffects + */ +const sounds = { + strike: new Audio("/static/sounds/strike.mp3"), + alarm: new Audio("/static/sounds/alarm.mp3"), +}; + +/** + * State + */ +const state = { + // Segment display used for telling the remaining time. + display: null, + + // Current game state. + gamestate: null, + + // Estimated timeout date when the timer should be stopped. + estimatedTimeout: new Date(), + + // Amount of strikes (wrong answers) the player has made. + strikes: 0, + + // If the alarm has been played already + alarmPlayed: false, +}; + +/** + * Play a given audio file + * @param {Audio} sound Audio object containing a sound. + */ +function playSound(sound) { + sound.load(); + sound.play(); +} + +/** + * Update the modules. + * @param {Array} List of puzzles + */ +function updateModules(puzzles) { + // Delete the modules when the game is inactive or in info mode. + if (state.gamestate == "INACTIVE" || state.gamestate == "INFO") { + document.getElementById("modules").innerHTML = ""; + return; + } + + const modulesElement = document.getElementById("modules"); + + // Update the modules. + const modules = []; + for (const puzzle of puzzles) { + const moduleElement = document.createElement("div"); + moduleElement.textContent = puzzle.address; + + // State: Solved + if (puzzle.solved === true) { + moduleElement.className = "solved"; + } + + // State: Unsolved + if (puzzle.solved === false) { + moduleElement.className = "unsolved"; + } + + // State: Needy + if (puzzle.solved === null) { + moduleElement.className = "needy"; + } + + modules.push(moduleElement); + } + + modulesElement.replaceChildren(...modules); +} + +/** + * Initialize the game state. + */ +function initializeGameState() { + updateGameState(); + setInterval(updateGameState, 500); +} + +/** + * Update the game state. + */ +function updateGameState() { + fetch("/status.json") + .then((res) => res.json()) + .then((data) => { + // Update the game state + state.gamestate = data.gamestate; + document.getElementById("gamestate").innerHTML = state.gamestate; + + // Reset the strike amount if the game is not running anymore. + if (state.gamestate != "GAME") { + state.strikeAmount = 0; + state.alarmPlayed = false; + } + + if (state.gamestate === "GAME") { + // Update the estimated timeout date + state.estimatedTimeout = new Date( + new Date().getTime() + data.timeleft * 1000 + ); + + // Calculate the total amount of strikes for all puzzles. + const newStrikes = data.puzzles + .map((p) => p.strikes) + .reduce((a, b) => a + b, 0); + + // Play a "buzzer" sound when a strike is made. + if (state.strikes < newStrikes) { + playSound(sounds.strike); + } + + // Play a "alarm" sound when the time is at 10 seconds. + if (data.timeleft <= 20 && data.timeleft > 19 && !state.alarmPlayed) { + playSound(sounds.alarm); + state.alarmPlayed = true; + } + + // Update the total amount of strikes + state.strikes = newStrikes; + } + + // Update the start/restart button visibility. + const startButton = document.querySelector("#buttonStart"); + const restartButton = document.querySelector("#buttonRestart"); + startButton.disabled = state.gamestate !== "DISCOVER"; + restartButton.disabled = state.gamestate !== "GAMEOVER"; + + // Update the modules + updateModules(data.puzzles); + }); +} + +/** + * When the start button is clicked. + */ +function onStartButtonClick() { + fetch("/start"); +} + +/** + * When the restart button is clicked. + */ +function onRestartButtonClick() { + fetch("/restart"); +} + +/** + * Initialize the segment display. + */ +function initializeSegmentDisplay() { + display = new SegmentDisplay("display"); + display.pattern = "##:##.#"; + display.displayAngle = 0; + display.digitHeight = 100; + display.digitWidth = display.digitHeight / 2; + display.digitDistance = display.digitHeight / 10; + display.segmentWidth = display.digitHeight / 10; + display.segmentDistance = display.digitHeight / 100; + display.segmentCount = 7; + display.cornerType = 1; + display.colorOn = "#ff0000"; + display.colorOff = "#ff000022"; + display.draw(); + + // Update the segment + setInterval(updateSegmentDisplay, 100); +} + +/** + * Update the segment display with the latest game data. + */ +function updateSegmentDisplay() { + // Do not update the timer when the game is not running. + if (state.gamestate != "GAME" || !state.estimatedTimeout) { + return; + } + + const timeLeft = (state.estimatedTimeout - new Date()) / 1000; + + // Set the display to 0 when there is no time left. + if (timeLeft <= 0) { + display.setValue("00:00.0"); + return; + } + + const integral = Math.floor(timeLeft); + const fractional = timeLeft - integral; + const minutes = Math.floor(integral / 60); + const seconds = integral % 60; + + const minutesStr = String(minutes).padStart(2, "0"); + const secondsStr = String(seconds).padStart(2, "0"); + const fractionalStr = String(Math.floor(fractional * 10)).padStart(1, "0"); + + display.setValue(`${minutesStr}:${secondsStr}.${fractionalStr}`); +} + +/** + * When the window is loaded. + */ +window.addEventListener("load", () => { + initializeGameState(); + initializeSegmentDisplay(); +}); diff --git a/python/static/debugscript.js b/python/static/js/debugscript.js similarity index 100% rename from python/static/debugscript.js rename to python/static/js/debugscript.js diff --git a/python/static/segment-display.js b/python/static/js/segment-display.js similarity index 100% rename from python/static/segment-display.js rename to python/static/js/segment-display.js diff --git a/python/static/sounds/alarm.mp3 b/python/static/sounds/alarm.mp3 new file mode 100644 index 0000000..4e2349f Binary files /dev/null and b/python/static/sounds/alarm.mp3 differ diff --git a/python/static/strike.mp3 b/python/static/sounds/strike.mp3 similarity index 100% rename from python/static/strike.mp3 rename to python/static/sounds/strike.mp3