From b2c5dec784bdcbfae5ecc8e3718bc2bda494a950 Mon Sep 17 00:00:00 2001 From: Steven Carpenter Date: Thu, 15 Aug 2024 14:24:11 -0400 Subject: [PATCH] Inital attempt and Commit --- LICENSE.txt | 22 ++++++++ css/style.css | 33 +++++++++++ index.html | 42 ++++++++++++++ js/app.js | 97 ++++++++++++++++++++++++++++++++ js/dialer.js | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 343 insertions(+) create mode 100644 LICENSE.txt create mode 100644 css/style.css create mode 100644 index.html create mode 100644 js/app.js create mode 100644 js/dialer.js diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..219b70f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2021 Kyle Simpson + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..1cfa456 --- /dev/null +++ b/css/style.css @@ -0,0 +1,33 @@ +#dialpad { + display: inline-grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: min-content; + grid-gap: 0.5rem; +} + +#dialpad > button { + font-size: 1.5rem; + padding: 0.5rem; + width: 3rem; + height: 3rem; + cursor: pointer; +} + +#dialpad > button:hover { + border-color: orange; +} + +#dialpad > button.highlighted { + border-color: blue; +} + +#results.hidden { + display: none; +} + +#starting-key-1, +#starting-key-2, +#hop-count, +#path-count { + font-weight: bold; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..112e267 --- /dev/null +++ b/index.html @@ -0,0 +1,42 @@ + + + +Knight's Dialer + + + + +

+ Hops to take: +

+ +
+ + + + + + + + + + + +
+ + + + + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..212d16f --- /dev/null +++ b/js/app.js @@ -0,0 +1,97 @@ +import Dialer from "./dialer.js"; + + +document.addEventListener("DOMContentLoaded",function ready(){ + var enterHopsEl = document.getElementById("enter-hop-count"); + var dialpadEl = document.getElementById("dialpad"); + var dialpadKeyEls = dialpadEl.querySelectorAll("button"); + var resultsEl = document.getElementById("results"); + var startingKey1El = document.getElementById("starting-key-1"); + var startingKey2El = document.getElementById("starting-key-2"); + var hopCountEl = document.getElementById("hop-count"); + var pathCountEl = document.getElementById("path-count"); + var countTimingEl = document.getElementById("count-timing"); + var acyclicPathsEl = document.getElementById("acyclic-paths"); + var pathsTimingEl = document.getElementById("paths-timing"); + + dialpadEl.addEventListener("mouseover",onHoverKey,false); + dialpadEl.addEventListener("mouseout",onHoverKey,false); + dialpadEl.addEventListener("click",onClickKey,false); + + + // ******************************** + + function onHoverKey(evt) { + var el = evt.target; + + // discover which keys (if any) can be reached from + // a hovered key + var reachable = ( + el.matches("button:hover") ? + Dialer.reachableKeys(Number(el.value)) : + [] + ); + + for (let keyEl of dialpadKeyEls) { + if (reachable.includes(Number(keyEl.value))) { + keyEl.classList.add("highlighted"); + } + else { + keyEl.classList.remove("highlighted"); + } + } + } + + function onClickKey(evt) { + var clickedEl = evt.target; + + // clicked on a dialpad key? + if (clickedEl.matches("#dialpad > button")) { + let startingKey = Number(clickedEl.value); + startingKey1El.innerText = startingKey; + startingKey2El.innerText = startingKey; + + let hopCount = Number(enterHopsEl.value) || 1; + enterHopsEl.value = hopCount; + hopCountEl.innerText = hopCount; + + + let startTiming = performance.now(); + // count the distinct paths (including cyclic paths) + let pathCount = Dialer.countPaths(startingKey,hopCount); + let endTiming = performance.now(); + printTiming(countTimingEl,Number(endTiming - startTiming) || 0); + pathCountEl.innerText = pathCount; + + startTiming = performance.now(); + let acyclicPaths = Dialer.listAcyclicPaths(startingKey); + endTiming = performance.now(); + printTiming(pathsTimingEl,Number(endTiming - startTiming) || 0); + if (acyclicPaths.length > 0) { + acyclicPathsEl.innerHTML = ""; + for (let path of acyclicPaths) { + let pathEl = document.createElement("div"); + pathEl.innerHTML = path.join(" ➡ "); + acyclicPathsEl.appendChild(pathEl); + } + } + else { + acyclicPathsEl.innerHTML = "-- none --"; + } + + resultsEl.classList.remove("hidden"); + } + } + + function printTiming(timingEl,ms) { + ms = Number(ms.toFixed(1)); + + if (ms >= 50) { + timingEl.innerHTML = `(${(ms / 1000).toFixed(2)} sec)`; + } + else { + timingEl.innerHTML = `(${ms.toFixed(1)} ms)` + } + } + +}); diff --git a/js/dialer.js b/js/dialer.js new file mode 100644 index 0000000..512cd16 --- /dev/null +++ b/js/dialer.js @@ -0,0 +1,149 @@ +export default { + reachableKeys, + countPaths, + listAcyclicPaths +}; +//-------------------// +// Constants // +//-------------------// + +const reachableKeysMap = new Map([ + [0, [4, 6]], // From key 0, you can reach keys 4 and 6 + [1, [6, 8]], // From key 1, you can reach keys 6 and 8 + [2, [9, 7]], // From key 2, you can reach keys 9 and 7 + [3, [8, 4]], // From key 3, you can reach keys 8 and 4 + [4, [3, 9, 0]], // From key 4, you can reach keys 3, 9, and 0 + [6, [1, 7, 0]], // From key 6, you can reach keys 1, 7, and 0 + [7, [2, 6]], // From key 7, you can reach keys 2 and 6 + [8, [1, 3]], // From key 8, you can reach keys 1 and 3 + [9, [2, 4]], // From key 9, you can reach keys 2 and 4 +]); + + +//-------------------// +// Utility Functions // +//-------------------// + +//TODO: P:1 Combine cyclic/acyclic DFS :ODOT// + +// Helper function to traverse nodes +function traverseDFS(currentDigit, processNode, endCondition, visited = new Set(), path = []) { + if (endCondition(currentDigit)) { + return processNode(); + } + + let count = 0; + let reachable = reachableKeys(currentDigit); + + for (let nextDigit of reachable) { + if (visited.has(nextDigit)) continue; + + // Mark the node as visited for acyclic paths + if (visited !== null) visited.add(nextDigit); + + // Add node to path for acyclic paths + if (path !== null) path.push(nextDigit); + + // Process node and/or count results + count += traverseDFS(nextDigit, processNode, endCondition, visited, path); + + // Backtrack for acyclic paths + if (path !== null) path.pop(); + if (visited !== null) visited.delete(nextDigit); + } + + return count; +} + +// Recursive DFS function for cyclic paths +function cyclicDFS(currentDigit, remainingHops) { + return traverseDFS( + currentDigit, + () => remainingHops === 0 ? 1 : 0, + () => remainingHops === 0, + null, // No visited nodes needed + null // No path needed + ); +} + +// Recursive DFS function for acyclic paths +function asyclicDFS(currentDigit, visited, path) { + return traverseDFS( + currentDigit, + () => { + if (path.length > 1) { + allPaths.push([...path]); + } + return 0; // Count not needed for acyclic paths + }, + () => false, // No end condition, continue until all paths are explored + visited, + path + ); +} + +// Quick Sort implementation to sort paths for acyclic path listing +function quickSort(arr, compareFn) { + if (arr.length <= 1) { + return arr; + } + + const pivot = arr[Math.floor(arr.length / 2)]; + const left = []; + const right = []; + const middle = []; + + for (const element of arr) { + if (compareFn(element, pivot) < 0) { + left.push(element); + } else if (compareFn(element, pivot) > 0) { + right.push(element); + } else { + middle.push(element); + } + } + + return [...quickSort(left, compareFn), ...middle, ...quickSort(right, compareFn)]; +} + +// Comparator function to compare path lengths +function comparePathLength(pathA, pathB) { + return pathA.length - pathB.length; +} + +//-------------------// +// Export Functions // +//-------------------// + +// Function to validate starting digit input and retrive the reachable values from the reachableKeysMap +function reachableKeys(startingDigit) { + //Check if startingDigit is an invalid digit or 5 + if (!reachableKeysMap.has(startingDigit)){ + return []; + } + //return the mapped values for startingDigit + return reachableKeysMap.get(startingDigit); +} + +// Function to build a list of all acyclic paths +function listAcyclicPaths(startingDigit) { + // Handle invalid cases + if (!reachableKeysMap.has(startingDigit)) { + return []; + } + + let allPaths = []; + asyclicDFS(startingDigit, new Set(), []); + + // Sort paths using Quick Sort based on their lengths + return quickSort(allPaths, comparePathLength); +} + +// Function to count number of total paths given a startingDigit and a hopCount +function countPaths(startingDigit, hopCount) { + // Handle invalid cases + if (!reachableKeysMap.has(startingDigit) || hopCount < 0) { + return 0; + } + return cyclicDFS(startingDigit, hopCount); +}