viewof gameConfig = {
const container = htl.html`<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 15px; padding: 15px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 12px; border: 2px solid #16A085;">
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-weight: 600; color: #16A085;">Level:</label>
<select id="levelSelect" style="padding: 6px 12px; border-radius: 6px; border: 1px solid #16A085; background: #0f3460; color: #e94560; font-weight: bold;">
<option value="1">Level 1: Flat Routing</option>
<option value="2">Level 2: Hierarchical Routing</option>
<option value="3">Level 3: Geographic Routing</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<label style="font-weight: 600; color: #16A085;">Protocol:</label>
<select id="protocolSelect" style="padding: 6px 12px; border-radius: 6px; border: 1px solid #16A085; background: #0f3460; color: #e94560; font-weight: bold;">
<option value="manual">Manual (Your Design)</option>
<option value="flooding">Flooding</option>
<option value="gossiping">Gossiping</option>
<option value="leach">LEACH</option>
<option value="directed">Directed Diffusion</option>
</select>
</div>
<button id="startGameBtn" style="padding: 8px 20px; background: linear-gradient(135deg, #16A085 0%, #1abc9c 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 700; box-shadow: 0 4px 15px rgba(22, 160, 133, 0.4);">Start Game</button>
<button id="sendPacketBtn" style="padding: 8px 20px; background: linear-gradient(135deg, #E67E22 0%, #f39c12 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 700; box-shadow: 0 4px 15px rgba(230, 126, 34, 0.4);">Send Packet</button>
<button id="resetGameBtn" style="padding: 8px 20px; background: linear-gradient(135deg, #95A5A6 0%, #bdc3c7 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 700;">Reset</button>
</div>`;
return container;
}
// Main WSN Route Optimizer Game
wsnRouteGame = {
const width = 800;
const height = 500;
const INITIAL_ENERGY = 100;
const TX_ENERGY_BASE = 2;
const RX_ENERGY = 0.5;
// Game state
let state = {
level: 1,
protocol: "manual",
nodes: [],
links: [],
selectedPath: [],
sinkNode: null,
sourceNodes: [],
clusterHeads: [],
round: 0,
score: 0,
packetsDelivered: 0,
packetsLost: 0,
totalEnergyUsed: 0,
gameActive: false
};
// Level configurations
const levelConfigs = {
1: {
name: "Flat Routing",
description: "All nodes equal. Design efficient paths using flooding, gossiping, or custom routes.",
nodeCount: 16,
sourceCount: 3,
objective: "Deliver 10 packets with <30% energy consumption",
targetDelivery: 10,
maxEnergyPercent: 30,
enableClustering: false
},
2: {
name: "Hierarchical Routing",
description: "Select cluster heads wisely. Balance aggregation benefits against CH energy drain.",
nodeCount: 20,
sourceCount: 4,
objective: "Deliver 15 packets, rotate cluster heads to extend lifetime",
targetDelivery: 15,
maxEnergyPercent: 40,
enableClustering: true
},
3: {
name: "Geographic Routing",
description: "Use location information for greedy forwarding. Handle voids and dead ends.",
nodeCount: 24,
sourceCount: 5,
objective: "Deliver 20 packets using location-based forwarding",
targetDelivery: 20,
maxEnergyPercent: 35,
enableClustering: false
}
};
function initializeNetwork(level) {
const config = levelConfigs[level];
state.nodes = [];
state.links = [];
state.selectedPath = [];
state.round = 0;
state.packetsDelivered = 0;
state.packetsLost = 0;
state.totalEnergyUsed = 0;
// Create sink node
state.sinkNode = {
id: 0, x: width / 2, y: 50,
energy: Infinity, isSink: true, isSource: false, isClusterHead: false
};
state.nodes.push(state.sinkNode);
// Create sensor nodes
const cols = Math.ceil(Math.sqrt(config.nodeCount));
const rows = Math.ceil(config.nodeCount / cols);
const cellWidth = (width - 100) / cols;
const cellHeight = (height - 200) / rows;
for (let i = 0; i < config.nodeCount; i++) {
const col = i % cols;
const row = Math.floor(i / cols);
state.nodes.push({
id: i + 1,
x: 50 + col * cellWidth + cellWidth/2 + (Math.random() - 0.5) * cellWidth * 0.4,
y: 150 + row * cellHeight + cellHeight/2 + (Math.random() - 0.5) * cellHeight * 0.4,
energy: INITIAL_ENERGY, isSink: false, isSource: false, isClusterHead: false
});
}
// Designate sources
state.sourceNodes = [];
const sensorNodes = state.nodes.filter(n => !n.isSink);
const bottomNodes = sensorNodes.sort((a, b) => b.y - a.y).slice(0, config.sourceCount);
bottomNodes.forEach(n => { n.isSource = true; state.sourceNodes.push(n); });
// Create links
createLinks();
state.level = level;
state.gameActive = true;
}
function createLinks() {
state.links = [];
const maxLinkDist = 150;
for (let i = 0; i < state.nodes.length; i++) {
for (let j = i + 1; j < state.nodes.length; j++) {
const n1 = state.nodes[i], n2 = state.nodes[j];
const dist = Math.sqrt((n1.x - n2.x) ** 2 + (n1.y - n2.y) ** 2);
if (dist < maxLinkDist) {
const quality = Math.max(0.5, 1 - (dist / maxLinkDist) * 0.5 + (Math.random() - 0.5) * 0.2);
state.links.push({ source: n1.id, target: n2.id, quality: Math.min(1, Math.max(0.3, quality)), active: false });
}
}
}
}
function getEnergyColor(energy) {
if (energy === Infinity) return "#16A085";
if (energy <= 0) return "#2c2c2c";
if (energy < 20) return "#E74C3C";
if (energy < 40) return "#E67E22";
if (energy < 60) return "#F1C40F";
return "#16A085";
}
// Create SVG
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "linear-gradient(180deg, #0f0c29 0%, #302b63 50%, #24243e 100%)")
.style("border-radius", "12px")
.style("border", "2px solid #16A085");
function render() {
svg.selectAll("*").remove();
const config = levelConfigs[state.level] || levelConfigs[1];
// Title
svg.append("text").attr("x", width / 2).attr("y", 25).attr("text-anchor", "middle")
.attr("font-size", "18px").attr("font-weight", "bold").attr("fill", "#16A085")
.text(`WSN Route Optimizer - ${config.name}`);
// Draw links
state.links.forEach(link => {
const source = state.nodes.find(n => n.id === link.source);
const target = state.nodes.find(n => n.id === link.target);
if (!source || !target) return;
const inPath = state.selectedPath.length > 1 &&
state.selectedPath.some((id, i) => {
if (i === state.selectedPath.length - 1) return false;
const nextId = state.selectedPath[i + 1];
return (id === link.source && nextId === link.target) ||
(id === link.target && nextId === link.source);
});
svg.append("line")
.attr("x1", source.x).attr("y1", source.y)
.attr("x2", target.x).attr("y2", target.y)
.attr("stroke", inPath ? "#00ff88" : (link.quality > 0.8 ? "rgba(22, 160, 133, 0.6)" : link.quality > 0.6 ? "rgba(241, 196, 15, 0.5)" : "rgba(231, 76, 60, 0.4)"))
.attr("stroke-width", inPath ? 3 : 1.5)
.attr("opacity", inPath ? 1 : 0.6);
});
// Draw nodes
state.nodes.forEach(node => {
const g = svg.append("g").attr("transform", `translate(${node.x}, ${node.y})`).style("cursor", "pointer");
const inPath = state.selectedPath.includes(node.id);
if (inPath) {
g.append("circle").attr("r", 22).attr("fill", "none").attr("stroke", "#00ff88").attr("stroke-width", 3).attr("opacity", 0.7);
}
let nodeSize = node.isSink ? 25 : (node.isSource ? 18 : 15);
g.append("circle").attr("r", nodeSize).attr("fill", getEnergyColor(node.energy)).attr("stroke", node.isSink ? "#16A085" : (node.isSource ? "#E67E22" : "#333")).attr("stroke-width", 2);
if (node.isSink) {
g.append("text").attr("y", 5).attr("text-anchor", "middle").attr("font-size", "12px").attr("font-weight", "bold").attr("fill", "#fff").text("SINK");
} else {
g.append("text").attr("y", 4).attr("text-anchor", "middle").attr("font-size", "10px").attr("font-weight", "bold").attr("fill", "#fff").text(node.id);
if (node.isSource) {
g.append("text").attr("y", -nodeSize - 5).attr("text-anchor", "middle").attr("font-size", "9px").attr("fill", "#E67E22").text("SRC");
}
}
});
// Stats
const statsY = height - 80;
svg.append("rect").attr("x", 10).attr("y", statsY).attr("width", 180).attr("height", 70).attr("fill", "rgba(0,0,0,0.7)").attr("rx", 8);
const stats = [
{ label: "Round", value: state.round },
{ label: "Delivered", value: state.packetsDelivered, color: "#16A085" },
{ label: "Lost", value: state.packetsLost, color: "#E74C3C" }
];
stats.forEach((stat, i) => {
svg.append("text").attr("x", 20).attr("y", statsY + 20 + i * 18).attr("font-size", "12px").attr("fill", "#aaa").text(`${stat.label}:`);
svg.append("text").attr("x", 130).attr("y", statsY + 20 + i * 18).attr("font-size", "12px").attr("font-weight", "bold").attr("fill", stat.color || "#fff").text(stat.value);
});
}
initializeNetwork(1);
render();
// Button handlers
const startBtn = document.querySelector("#startGameBtn");
const sendBtn = document.querySelector("#sendPacketBtn");
const resetBtn = document.querySelector("#resetGameBtn");
const levelSelect = document.querySelector("#levelSelect");
if (startBtn) startBtn.onclick = () => { initializeNetwork(parseInt(levelSelect?.value || "1")); render(); };
if (resetBtn) resetBtn.onclick = () => { initializeNetwork(parseInt(levelSelect?.value || "1")); render(); };
if (levelSelect) levelSelect.onchange = () => { initializeNetwork(parseInt(levelSelect.value)); render(); };
return svg.node();
}