Created
January 28, 2025 12:19
-
-
Save secdev02/6c5bc601d2bd1252c96aa180c37dfcef to your computer and use it in GitHub Desktop.
D3 JS - Model - Hunt the Wumpus Style Game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
.node { | |
stroke: #fff; | |
stroke-width: 1.5px; | |
cursor: pointer; | |
} | |
.link { | |
stroke: #999; | |
stroke-opacity: 0.6; | |
} | |
.hidden { | |
display: none; | |
} | |
.revealed { | |
opacity: 1; | |
} | |
.unexplored { | |
opacity: 0.3; | |
} | |
.game-controls { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
background: white; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 5px; | |
} | |
.game-stats { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
background: white; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 5px; | |
} | |
.game-over { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background: rgba(0, 0, 0, 0.8); | |
color: white; | |
padding: 20px; | |
border-radius: 10px; | |
text-align: center; | |
display: none; | |
} | |
.danger-indicator { | |
color: red; | |
font-weight: bold; | |
} | |
</style> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> | |
</head> | |
<body> | |
<div class="game-controls"> | |
<h3>Network Explorer</h3> | |
<button id="restartGame">Restart Game</button> | |
<p>Click nodes to explore the network</p> | |
<p class="danger-indicator">⚠️ Warning: Dangerous nodes exist!</p> | |
</div> | |
<div class="game-stats"> | |
<div>Explored Nodes: <span id="exploredCount">0</span></div> | |
<div>Connected Nodes: <span id="connectedCount">0</span></div> | |
<div>Danger Sense: <span id="dangerSense">Safe</span></div> | |
</div> | |
<div class="game-over"> | |
<h2 id="gameOverText"></h2> | |
<p id="gameOverStats"></p> | |
<button onclick="location.reload()">Play Again</button> | |
</div> | |
<script> | |
class NetworkGame { | |
constructor() { | |
this.width = window.innerWidth; | |
this.height = window.innerHeight; | |
this.nodes = []; | |
this.links = []; | |
this.revealedNodes = new Set(); | |
this.gameOver = false; | |
this.dangerousNodes = new Set(); | |
this.initializeSVG(); | |
this.generateNetwork(); | |
this.setupSimulation(); | |
this.setupEventListeners(); | |
} | |
initializeSVG() { | |
this.svg = d3.select("body") | |
.append("svg") | |
.attr("width", this.width) | |
.attr("height", this.height); | |
} | |
generateNetwork() { | |
// Generate 100 nodes | |
for (let i = 0; i < 100; i++) { | |
this.nodes.push({ | |
id: `Node${i}`, | |
rdp: Math.random() < 0.3, | |
ssh: Math.random() < 0.3, | |
winrm: Math.random() < 0.3, | |
wmic: Math.random() < 0.3, | |
sccm: Math.random() < 0.3, | |
revealed: false, | |
isDangerous: false | |
}); | |
} | |
// Set random nodes as dangerous | |
const shuffled = [...this.nodes]; | |
for (let i = 0; i < 5; i++) { | |
const randomIndex = Math.floor(Math.random() * shuffled.length); | |
shuffled[randomIndex].isDangerous = true; | |
this.dangerousNodes.add(shuffled[randomIndex].id); | |
} | |
// Generate links between nodes | |
for (let i = 0; i < this.nodes.length; i++) { | |
const numConnections = Math.floor(Math.random() * 3) + 1; | |
for (let j = 0; j < numConnections; j++) { | |
const target = Math.floor(Math.random() * this.nodes.length); | |
if (target !== i) { | |
this.links.push({ | |
source: this.nodes[i].id, | |
target: this.nodes[target].id, | |
revealed: false | |
}); | |
} | |
} | |
} | |
// Reveal starting node | |
const startNode = this.nodes[0]; | |
startNode.revealed = true; | |
this.revealedNodes.add(startNode.id); | |
this.updateConnectedNodes(startNode); | |
} | |
setupSimulation() { | |
this.simulation = d3.forceSimulation(this.nodes) | |
.force("link", d3.forceLink(this.links).id(d => d.id).distance(100)) | |
.force("charge", d3.forceManyBody().strength(-300)) | |
.force("center", d3.forceCenter(this.width / 2, this.height / 2)) | |
.force("collision", d3.forceCollide().radius(20)); | |
// Create links | |
this.link = this.svg.append("g") | |
.selectAll("line") | |
.data(this.links) | |
.enter() | |
.append("line") | |
.attr("class", "link unexplored"); | |
// Create nodes | |
this.node = this.svg.append("g") | |
.selectAll("circle") | |
.data(this.nodes) | |
.enter() | |
.append("circle") | |
.attr("class", "node unexplored") | |
.attr("r", 8) | |
.on("click", (event, d) => this.handleNodeClick(d)); | |
// Add labels | |
this.labels = this.svg.append("g") | |
.selectAll("text") | |
.data(this.nodes) | |
.enter() | |
.append("text") | |
.text(d => d.id) | |
.attr("font-size", "8px") | |
.attr("dx", 10) | |
.attr("dy", 4) | |
.attr("class", "unexplored"); | |
this.simulation.on("tick", () => this.updatePositions()); | |
this.updateVisibility(); | |
} | |
updatePositions() { | |
this.link | |
.attr("x1", d => d.source.x) | |
.attr("y1", d => d.source.y) | |
.attr("x2", d => d.target.x) | |
.attr("y2", d => d.target.y); | |
this.node | |
.attr("cx", d => d.x) | |
.attr("cy", d => d.y); | |
this.labels | |
.attr("x", d => d.x) | |
.attr("y", d => d.y); | |
} | |
updateVisibility() { | |
this.node | |
.attr("class", d => { | |
if (d.revealed) return "node revealed"; | |
if (this.isConnectedToRevealed(d)) return "node"; | |
return "node hidden"; | |
}) | |
.attr("fill", d => { | |
if (!d.revealed && this.isConnectedToRevealed(d)) return "#666"; | |
if (d.isDangerous && d.revealed) return "red"; | |
return this.getNodeColor(d); | |
}); | |
this.labels | |
.attr("class", d => { | |
if (d.revealed) return "revealed"; | |
if (this.isConnectedToRevealed(d)) return ""; | |
return "hidden"; | |
}); | |
this.link | |
.attr("class", d => { | |
if (this.revealedNodes.has(d.source.id) && this.revealedNodes.has(d.target.id)) | |
return "link revealed"; | |
if (this.revealedNodes.has(d.source.id) || this.revealedNodes.has(d.target.id)) | |
return "link"; | |
return "link hidden"; | |
}); | |
} | |
getNodeColor(node) { | |
if (node.rdp) return "#ff7f0e"; | |
if (node.ssh) return "#2ca02c"; | |
if (node.winrm) return "#d62728"; | |
if (node.wmic) return "#9467bd"; | |
return "#8c564b"; | |
} | |
isConnectedToRevealed(node) { | |
return this.links.some(link => | |
(link.source.id === node.id && this.revealedNodes.has(link.target.id)) || | |
(link.target.id === node.id && this.revealedNodes.has(link.source.id)) | |
); | |
} | |
handleNodeClick(node) { | |
if (this.gameOver || !this.isConnectedToRevealed(node)) return; | |
if (node.isDangerous) { | |
this.endGame(false); | |
return; | |
} | |
node.revealed = true; | |
this.revealedNodes.add(node.id); | |
this.updateConnectedNodes(node); | |
this.updateVisibility(); | |
this.updateStats(); | |
if (this.revealedNodes.size === this.nodes.length) { | |
this.endGame(true); | |
} | |
} | |
updateConnectedNodes(node) { | |
const connectedNodes = this.links | |
.filter(link => link.source.id === node.id || link.target.id === node.id) | |
.map(link => link.source.id === node.id ? link.target : link.source); | |
const hasDangerousNeighbor = connectedNodes.some(n => this.dangerousNodes.has(n.id)); | |
document.getElementById("dangerSense").textContent = hasDangerousNeighbor ? "⚠️ DANGER NEARBY!" : "Safe"; | |
document.getElementById("dangerSense").style.color = hasDangerousNeighbor ? "red" : "green"; | |
} | |
updateStats() { | |
document.getElementById("exploredCount").textContent = this.revealedNodes.size; | |
const connectedCount = this.nodes.filter(n => !n.revealed && this.isConnectedToRevealed(n)).length; | |
document.getElementById("connectedCount").textContent = connectedCount; | |
} | |
endGame(won) { | |
this.gameOver = true; | |
const gameOverDiv = document.querySelector(".game-over"); | |
const gameOverText = document.getElementById("gameOverText"); | |
const gameOverStats = document.getElementById("gameOverStats"); | |
if (won) { | |
gameOverText.textContent = "Congratulations! You've explored the entire network!"; | |
} else { | |
gameOverText.textContent = "Game Over! You've encountered a dangerous node!"; | |
} | |
gameOverStats.textContent = `Nodes explored: ${this.revealedNodes.size} of ${this.nodes.length}`; | |
gameOverDiv.style.display = "block"; | |
// Reveal all dangerous nodes | |
this.nodes.forEach(node => { | |
if (node.isDangerous) node.revealed = true; | |
}); | |
this.updateVisibility(); | |
} | |
setupEventListeners() { | |
document.getElementById("restartGame").addEventListener("click", () => { | |
location.reload(); | |
}); | |
} | |
} | |
// Start the game | |
const game = new NetworkGame(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment