Skip to content

Instantly share code, notes, and snippets.

@secdev02
Created January 28, 2025 12:19
Show Gist options
  • Save secdev02/6c5bc601d2bd1252c96aa180c37dfcef to your computer and use it in GitHub Desktop.
Save secdev02/6c5bc601d2bd1252c96aa180c37dfcef to your computer and use it in GitHub Desktop.
D3 JS - Model - Hunt the Wumpus Style Game
<!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