viewof computePlacementGame = {
// ===========================================================================
// COMPUTE PLACEMENT GAME - EDUCATIONAL IoT ARCHITECTURE GAME
// ===========================================================================
// An interactive game teaching compute tier selection through scenarios:
// - 15+ workload scenarios across 3 levels
// - Edge: real-time control, safety-critical, offline operation
// - Fog: aggregation, regional analytics, protocol translation
// - Cloud: ML training, historical analytics, global dashboards
// - Hybrid: split processing scenarios
//
// IEEE Color Palette:
// Navy: #2C3E50 (primary, cloud tier)
// Teal: #16A085 (secondary, fog tier)
// Orange: #E67E22 (highlights, edge tier)
// Gray: #7F8C8D (neutral)
// Green: #27AE60 (success/correct)
// Red: #E74C3C (error/incorrect)
// ===========================================================================
const d3 = await require("d3@7");
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 1000,
height: 700,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6",
blue: "#3498DB"
},
tiers: {
edge: {
name: "EDGE",
color: "#E67E22",
latency: "1-10ms",
bandwidth: "None (local)",
compute: "Limited",
cost: "$5/device/mo",
icon: "device"
},
fog: {
name: "FOG",
color: "#16A085",
latency: "20-100ms",
bandwidth: "LAN only",
compute: "Moderate",
cost: "$30/gateway/mo",
icon: "gateway"
},
cloud: {
name: "CLOUD",
color: "#2C3E50",
latency: "100-500ms",
bandwidth: "WAN required",
compute: "Unlimited",
cost: "$100+/mo",
icon: "cloud"
}
}
};
// ---------------------------------------------------------------------------
// SCENARIOS DATABASE - 15+ scenarios across 3 levels
// ---------------------------------------------------------------------------
const scenarios = {
// LEVEL 1: Basic scenarios (clear-cut answers)
level1: [
{
id: "l1_emergency_stop",
title: "Emergency Stop Button",
description: "A factory robot's emergency stop must halt all motion within 50ms when a worker enters the danger zone. The system uses proximity sensors and must work even if network connectivity is lost.",
requirements: {
latency: "<50ms (safety-critical)",
bandwidth: "1 KB/s sensor data",
compute: "Simple threshold check",
storage: "None needed",
connectivity: "Must work offline"
},
optimal: "edge",
acceptable: [],
feedback: {
edge: "Correct! Safety-critical systems requiring <50ms response and offline operation MUST process at the edge. Network latency to fog (20-100ms) or cloud (100-500ms) is too slow for worker safety.",
fog: "Incorrect. Fog latency (20-100ms) exceeds the 50ms safety requirement. Also, if network fails, the safety system would stop working - unacceptable for life-safety applications.",
cloud: "Incorrect. Cloud round-trip (100-500ms) is 2-10x slower than required. Never route safety-critical decisions through the cloud."
},
costImpact: { edge: 5, fog: 30, cloud: 100 },
latencyImpact: { edge: 5, fog: 60, cloud: 250 }
},
{
id: "l1_ml_training",
title: "Neural Network Training",
description: "Train a deep learning model using 6 months of historical sensor data (500 GB) to predict equipment failures. Training takes 48 hours on GPU clusters and only runs monthly.",
requirements: {
latency: "Hours acceptable",
bandwidth: "One-time upload",
compute: "GPU cluster (100+ TFLOPS)",
storage: "500 GB historical data",
connectivity: "Continuous during training"
},
optimal: "cloud",
acceptable: [],
feedback: {
cloud: "Correct! ML training with massive datasets needs cloud GPU clusters. Edge/fog devices lack the compute power (100+ TFLOPS) and storage (500GB) required.",
fog: "Incorrect. Fog gateways lack GPU clusters for deep learning. A Raspberry Pi-class device would take months to train this model.",
edge: "Incorrect. Edge MCUs cannot store 500GB or perform matrix operations required for neural network training."
},
costImpact: { edge: 999, fog: 500, cloud: 150 },
latencyImpact: { edge: 999999, fog: 999999, cloud: 172800000 }
},
{
id: "l1_temp_monitoring",
title: "HVAC Temperature Logging",
description: "Record temperature readings every 5 minutes from 50 building sensors for energy efficiency reports generated weekly. Data must be stored for 5 years for compliance.",
requirements: {
latency: "Minutes acceptable",
bandwidth: "0.1 KB per reading",
compute: "Simple averaging",
storage: "5 years retention",
connectivity: "Intermittent OK"
},
optimal: "cloud",
acceptable: ["fog"],
feedback: {
cloud: "Correct! Long-term storage (5 years) and batch analytics favor cloud. Low bandwidth (50 sensors x 0.1KB x 288/day = 1.4 MB/day) makes upload economical.",
fog: "Acceptable but suboptimal. Fog can aggregate but 5-year storage on local gateways is expensive and risky (hardware failures).",
edge: "Incorrect. Edge sensors cannot store 5 years of data or generate cross-building efficiency reports."
},
costImpact: { edge: 200, fog: 80, cloud: 50 },
latencyImpact: { edge: 5, fog: 50, cloud: 300 }
},
{
id: "l1_collision_avoidance",
title: "Forklift Collision Avoidance",
description: "Autonomous forklifts in a warehouse must detect obstacles (humans, other vehicles, shelving) and stop within 100ms. Camera-based detection runs at 30 fps.",
requirements: {
latency: "<100ms (safety)",
bandwidth: "30 fps video = 50 Mbps",
compute: "Object detection ML",
storage: "None real-time",
connectivity: "Unreliable in warehouse"
},
optimal: "edge",
acceptable: [],
feedback: {
edge: "Correct! Safety-critical collision avoidance needs edge processing. The forklift's onboard GPU runs inference in 30ms, leaving margin for actuation.",
fog: "Incorrect. Warehouse Wi-Fi is unreliable (metal shelving interference). Network latency variability (20-200ms) risks safety.",
cloud: "Incorrect. Uploading 50 Mbps video per forklift is expensive ($500+/mo) and adds 100-500ms latency - too slow for collision avoidance."
},
costImpact: { edge: 100, fog: 200, cloud: 600 },
latencyImpact: { edge: 30, fog: 80, cloud: 350 }
},
{
id: "l1_global_dashboard",
title: "Executive Fleet Dashboard",
description: "Display real-time status of 10,000 delivery vehicles across 50 cities on a single dashboard. Data refreshes every 30 seconds with daily/weekly trend analytics.",
requirements: {
latency: "30 seconds acceptable",
bandwidth: "1 KB per vehicle per update",
compute: "Aggregation + visualization",
storage: "90 days for trends",
connectivity: "Always connected"
},
optimal: "cloud",
acceptable: [],
feedback: {
cloud: "Correct! Global aggregation across 50 cities requires cloud. No single fog node can see all vehicles. 30-second latency is easily met.",
fog: "Incorrect. Fog nodes are regional - no single gateway can aggregate 10,000 vehicles across 50 cities. Would need cloud for global view.",
edge: "Incorrect. Vehicle edge units cannot generate cross-fleet dashboards or store 90 days of fleet-wide data."
},
costImpact: { edge: 999, fog: 300, cloud: 200 },
latencyImpact: { edge: 999999, fog: 500, cloud: 400 }
}
],
// LEVEL 2: Intermediate scenarios (requires trade-off analysis)
level2: [
{
id: "l2_video_analytics",
title: "Retail Customer Counting",
description: "Count customers entering/exiting 500 stores in real-time for staffing decisions. Store managers need per-store counts in <5 seconds. Corporate needs hourly aggregates.",
requirements: {
latency: "<5s for store view",
bandwidth: "Camera feeds = 5 Mbps/store",
compute: "Person detection AI",
storage: "30-day trends",
connectivity: "Usually available"
},
optimal: "fog",
acceptable: ["edge"],
feedback: {
fog: "Correct! Fog gateway in each store runs person detection locally (meeting <5s), sends counts to cloud for aggregates. Reduces 2.5 Gbps total to <1 Mbps.",
edge: "Acceptable with smart cameras. Edge cameras with AI chips can count locally, but fog provides backup and cross-camera deduplication.",
cloud: "Incorrect. Uploading 5 Mbps x 500 stores = 2.5 Gbps to cloud costs $50K+/month. Fog reduces this 99%."
},
costImpact: { edge: 150, fog: 80, cloud: 500 },
latencyImpact: { edge: 100, fog: 500, cloud: 3000 }
},
{
id: "l2_predictive_maintenance",
title: "Vibration-Based Failure Prediction",
description: "Monitor 200 motors using vibration sensors (10 KHz sampling). Detect anomalies for predictive maintenance. Critical failures need alerts in <1 minute; maintenance scheduling can wait hours.",
requirements: {
latency: "<1 min for anomaly alerts",
bandwidth: "10 KB/s per motor raw",
compute: "FFT + anomaly detection",
storage: "6 months for ML training",
connectivity: "Factory LAN stable"
},
optimal: "fog",
acceptable: ["edge"],
feedback: {
fog: "Correct! Fog gateway runs FFT and anomaly detection locally (<1 min). Sends features (not raw data) to cloud for ML model updates. 99% bandwidth reduction.",
edge: "Acceptable for simple threshold alerts. But edge MCUs struggle with FFT on 10 KHz data. Fog handles complex signal processing better.",
cloud: "Suboptimal. Works technically (LAN is stable), but uploads 200 x 10 KB/s = 2 MB/s unnecessarily. Local FFT at fog is more efficient."
},
costImpact: { edge: 50, fog: 60, cloud: 150 },
latencyImpact: { edge: 100, fog: 500, cloud: 5000 }
},
{
id: "l2_smart_agriculture",
title: "Irrigation Decision System",
description: "Control irrigation valves based on soil moisture, weather forecasts, and crop models. Sensors report every 15 minutes. System must work during 4-hour cellular outages common in rural areas.",
requirements: {
latency: "15 minutes acceptable",
bandwidth: "Minimal sensor data",
compute: "Rule engine + scheduling",
storage: "Season history",
connectivity: "Intermittent (rural)"
},
optimal: "fog",
acceptable: [],
feedback: {
fog: "Correct! Fog gateway stores rules and schedules locally, operates during 4-hour outages. Syncs with cloud when connected for weather forecasts and crop model updates.",
edge: "Incorrect. Individual sensors lack the compute for crop models and cannot coordinate multiple valves. System needs centralized local intelligence.",
cloud: "Incorrect. 4-hour outages would stop irrigation decisions. Crops don't wait for connectivity. Local autonomy is required."
},
costImpact: { edge: 100, fog: 70, cloud: 50 },
latencyImpact: { edge: 100, fog: 1000, cloud: 15000 }
},
{
id: "l2_voice_assistant",
title: "Industrial Voice Commands",
description: "Workers on a noisy factory floor use voice commands (\"Robot arm, move left\"). Commands must execute in <500ms. Privacy rules prohibit sending audio to external clouds.",
requirements: {
latency: "<500ms response",
bandwidth: "16 KB/s audio stream",
compute: "Speech recognition AI",
storage: "Local command vocab",
connectivity: "Available but restricted"
},
optimal: "fog",
acceptable: ["edge"],
feedback: {
fog: "Correct! Local fog server with speech recognition (Raspberry Pi + Vosk) meets <500ms, keeps audio on-premises for privacy. No external cloud needed.",
edge: "Acceptable with modern edge AI chips. NVIDIA Jetson Nano can run speech recognition locally on each workstation.",
cloud: "Incorrect. Privacy rules prohibit external audio transmission. Also, noisy environment + network latency could exceed 500ms."
},
costImpact: { edge: 200, fog: 100, cloud: 999 },
latencyImpact: { edge: 150, fog: 200, cloud: 999999 }
},
{
id: "l2_protocol_translation",
title: "Legacy Equipment Integration",
description: "Connect 50 legacy PLCs using Modbus RTU (serial) to a cloud monitoring platform. PLCs cannot be modified. Cloud platform only accepts MQTT over TLS.",
requirements: {
latency: "Seconds acceptable",
bandwidth: "1 KB/s per PLC",
compute: "Protocol conversion",
storage: "Buffer during outages",
connectivity: "Ethernet available"
},
optimal: "fog",
acceptable: [],
feedback: {
fog: "Correct! Fog gateway performs Modbus RTU → MQTT translation. Cloud cannot speak Modbus, edge sensors don't have TCP/IP stacks. Gateway bridges the gap.",
edge: "Incorrect. Edge PLCs speak Modbus RTU only - they cannot run MQTT clients or TLS encryption. Protocol translation requires a gateway.",
cloud: "Incorrect. Cloud cannot directly connect to serial Modbus devices. Physical gateway required for protocol translation."
},
costImpact: { edge: 999, fog: 80, cloud: 999 },
latencyImpact: { edge: 999999, fog: 500, cloud: 999999 }
}
],
// LEVEL 3: Advanced scenarios (complex trade-offs, hybrid solutions)
level3: [
{
id: "l3_autonomous_vehicle",
title: "Self-Driving Car Perception",
description: "Process LiDAR, cameras, and radar for autonomous driving. Collision detection needs <20ms. Route planning uses cloud maps. Vehicle must function in tunnels (no connectivity).",
requirements: {
latency: "<20ms for collision",
bandwidth: "1 GB/s sensor data",
compute: "Multi-sensor fusion AI",
storage: "Local HD maps",
connectivity: "Intermittent (tunnels)"
},
optimal: "hybrid_edge_cloud",
acceptable: ["edge"],
feedback: {
hybrid_edge_cloud: "Excellent! This is a textbook hybrid case: Edge handles safety-critical perception (<20ms), downloads HD maps from cloud when connected, operates autonomously in tunnels.",
edge: "Partially correct. Edge must handle collision detection, but pure edge misses cloud benefits: map updates, fleet learning, route optimization.",
fog: "Incorrect. There's no fog layer in a moving vehicle on public roads. Edge + Cloud is the architecture.",
cloud: "Incorrect. Cloud cannot meet 20ms collision detection. The vehicle would crash before cloud responds."
},
costImpact: { edge: 500, fog: 999, cloud: 999, hybrid_edge_cloud: 350 },
latencyImpact: { edge: 15, fog: 999999, cloud: 999999, hybrid_edge_cloud: 15 }
},
{
id: "l3_smart_grid",
title: "Power Grid Frequency Regulation",
description: "Balance electricity grid frequency by coordinating 1,000 distributed batteries. Grid instability must be corrected in <100ms. But optimal dispatch requires 15-minute-ahead forecasting from all batteries.",
requirements: {
latency: "<100ms for frequency response",
bandwidth: "Real-time grid metrics",
compute: "Control loops + optimization",
storage: "Historical load patterns",
connectivity: "Mostly reliable"
},
optimal: "hybrid_fog_cloud",
acceptable: ["fog"],
feedback: {
hybrid_fog_cloud: "Excellent! Fog controllers handle <100ms frequency response locally. Cloud runs 15-minute optimization with full fleet visibility, pushes schedules to fog.",
fog: "Partially correct. Fog handles real-time, but 1,000-battery optimization across regions needs cloud's global view and compute.",
edge: "Incorrect. Individual batteries cannot see grid-wide frequency or coordinate with 999 other batteries.",
cloud: "Incorrect. Cloud alone cannot meet 100ms frequency response - grid would destabilize waiting for cloud round-trip."
},
costImpact: { edge: 300, fog: 150, cloud: 200, hybrid_fog_cloud: 120 },
latencyImpact: { edge: 50, fog: 80, cloud: 500, hybrid_fog_cloud: 80 }
},
{
id: "l3_medical_monitoring",
title: "ICU Patient Monitoring",
description: "Monitor vital signs (ECG, SpO2, BP) of 50 ICU patients. Arrhythmia detection needs <500ms alert. Data must be available hospital-wide for any doctor. HIPAA requires on-premises data.",
requirements: {
latency: "<500ms for critical alerts",
bandwidth: "50 KB/s per patient",
compute: "Arrhythmia detection AI",
storage: "On-premises (HIPAA)",
connectivity: "Hospital network reliable"
},
optimal: "fog",
acceptable: ["hybrid_edge_fog"],
feedback: {
fog: "Correct! Hospital fog server (on-premises private cloud) runs arrhythmia AI, provides hospital-wide access, maintains HIPAA compliance. No external cloud needed.",
hybrid_edge_fog: "Also correct! Edge bedside monitors provide immediate alerts, fog server coordinates hospital-wide view. Both stay on-premises.",
edge: "Partially correct for alerts, but doctors need cross-patient dashboards. Pure edge lacks hospital-wide data access.",
cloud: "Incorrect. HIPAA prohibits sending raw vitals to external cloud without patient consent and extensive safeguards."
},
costImpact: { edge: 150, fog: 100, cloud: 999, hybrid_edge_fog: 120 },
latencyImpact: { edge: 50, fog: 100, cloud: 999999, hybrid_edge_fog: 50 }
},
{
id: "l3_fleet_routing",
title: "Dynamic Delivery Routing",
description: "Optimize routes for 500 delivery trucks in real-time based on traffic, new orders, and driver breaks. Route updates every 5 minutes. But truck must navigate for 2 hours if connectivity lost.",
requirements: {
latency: "5 minutes for new routes",
bandwidth: "GPS + telemetry = 10 KB/min",
compute: "Vehicle routing optimization",
storage: "Local maps + routes",
connectivity: "Cellular (urban reliable)"
},
optimal: "hybrid_edge_cloud",
acceptable: ["cloud"],
feedback: {
hybrid_edge_cloud: "Excellent! Cloud runs fleet-wide optimization every 5 minutes, pushes routes to truck. Edge stores routes locally, navigates during 2-hour outages.",
cloud: "Partially correct. Cloud optimization is ideal, but trucks need local route storage for offline navigation.",
fog: "Incorrect. There's no regional fog layer here - trucks are mobile. Architecture is Edge (truck) + Cloud (fleet optimization).",
edge: "Incorrect. Individual trucks cannot optimize across 500 vehicles or access real-time traffic for other areas."
},
costImpact: { edge: 100, fog: 999, cloud: 150, hybrid_edge_cloud: 120 },
latencyImpact: { edge: 30000, fog: 999999, cloud: 300000, hybrid_edge_cloud: 300000 }
},
{
id: "l3_augmented_reality",
title: "Industrial AR Maintenance",
description: "AR headsets guide technicians through equipment repair with overlaid instructions. Must track headset position in <20ms for smooth overlay. Repair manuals (10GB) stored centrally.",
requirements: {
latency: "<20ms for tracking",
bandwidth: "Camera + depth = 50 Mbps",
compute: "SLAM + rendering",
storage: "10GB manuals (central)",
connectivity: "Factory Wi-Fi available"
},
optimal: "hybrid_edge_fog",
acceptable: ["edge"],
feedback: {
hybrid_edge_fog: "Excellent! Edge (headset) runs SLAM for <20ms tracking. Fog server stores 10GB manuals, streams relevant pages. Avoids cloud latency.",
edge: "Partially correct for tracking, but 10GB manuals won't fit on headset. Need fog/cloud for content delivery.",
fog: "Partially correct. Fog could run SLAM, but 20ms over Wi-Fi is risky with interference. Edge tracking is safer.",
cloud: "Incorrect. Cloud latency (100-500ms) causes AR overlay to lag behind head movement - causes motion sickness."
},
costImpact: { edge: 300, fog: 150, cloud: 200, hybrid_edge_fog: 180 },
latencyImpact: { edge: 15, fog: 50, cloud: 300, hybrid_edge_fog: 15 }
}
]
};
// ---------------------------------------------------------------------------
// GAME STATE
// ---------------------------------------------------------------------------
let state = {
level: 1,
currentScenarioIndex: 0,
score: 0,
totalQuestions: 0,
totalCostSaved: 0,
answers: [],
gameComplete: false,
showingFeedback: false,
selectedTier: null
};
// ---------------------------------------------------------------------------
// CONTAINER SETUP
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// GAME HEADER
// ---------------------------------------------------------------------------
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${config.colors.navy} 0%, #1a252f 100%)`)
.style("padding", "20px")
.style("border-radius", "12px 12px 0 0")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center");
const titleSection = header.append("div");
titleSection.append("h2")
.style("margin", "0")
.style("color", config.colors.white)
.style("font-size", "20px")
.text("Compute Placement Challenge");
const levelBadge = titleSection.append("span")
.style("display", "inline-block")
.style("padding", "4px 12px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border-radius", "12px")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("margin-left", "10px")
.text("Level 1: Fundamentals");
const scoreSection = header.append("div")
.style("text-align", "right");
const scoreDisplay = scoreSection.append("div")
.style("color", config.colors.white)
.style("font-size", "24px")
.style("font-weight", "bold")
.text("0 / 0");
const efficiencyDisplay = scoreSection.append("div")
.style("color", config.colors.lightGray)
.style("font-size", "12px")
.text("Efficiency: --");
// ---------------------------------------------------------------------------
// MAIN GAME AREA
// ---------------------------------------------------------------------------
const gameArea = container.append("div")
.style("background", config.colors.lightGray)
.style("padding", "20px")
.style("min-height", "500px");
// Scenario Card
const scenarioCard = gameArea.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
const scenarioTitle = scenarioCard.append("h3")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy);
const scenarioDesc = scenarioCard.append("p")
.style("color", config.colors.gray)
.style("line-height", "1.6")
.style("margin-bottom", "15px");
// Requirements Grid
const reqGrid = scenarioCard.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(180px, 1fr))")
.style("gap", "10px")
.style("margin-bottom", "15px");
// Tier Selection Buttons
const tierButtonsContainer = gameArea.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "20px")
.style("margin-bottom", "20px")
.style("flex-wrap", "wrap");
function createTierButton(tier, tierConfig) {
const btn = tierButtonsContainer.append("button")
.attr("class", `tier-btn tier-btn-${tier}`)
.style("padding", "20px 30px")
.style("background", config.colors.white)
.style("border", `3px solid ${tierConfig.color}`)
.style("border-radius", "12px")
.style("cursor", "pointer")
.style("min-width", "150px")
.style("transition", "all 0.2s")
.on("click", () => selectTier(tier))
.on("mouseenter", function() {
if (!state.showingFeedback) {
d3.select(this)
.style("background", tierConfig.color)
.style("transform", "scale(1.05)");
d3.select(this).selectAll("*").style("color", config.colors.white);
}
})
.on("mouseleave", function() {
if (!state.showingFeedback && state.selectedTier !== tier) {
d3.select(this)
.style("background", config.colors.white)
.style("transform", "scale(1)");
d3.select(this).select(".tier-name").style("color", tierConfig.color);
d3.select(this).select(".tier-details").style("color", config.colors.gray);
}
});
btn.append("div")
.attr("class", "tier-name")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("color", tierConfig.color)
.style("margin-bottom", "5px")
.text(tierConfig.name);
btn.append("div")
.attr("class", "tier-details")
.style("font-size", "11px")
.style("color", config.colors.gray)
.html(`${tierConfig.latency}<br>${tierConfig.compute}`);
return btn;
}
// Create tier buttons
createTierButton("edge", config.tiers.edge);
createTierButton("fog", config.tiers.fog);
createTierButton("cloud", config.tiers.cloud);
// Hybrid button (appears in Level 3)
const hybridBtn = tierButtonsContainer.append("button")
.attr("class", "tier-btn tier-btn-hybrid")
.style("padding", "20px 30px")
.style("background", config.colors.white)
.style("border", `3px solid ${config.colors.purple}`)
.style("border-radius", "12px")
.style("cursor", "pointer")
.style("min-width", "150px")
.style("display", "none")
.style("transition", "all 0.2s")
.on("click", () => selectTier("hybrid"))
.on("mouseenter", function() {
if (!state.showingFeedback) {
d3.select(this)
.style("background", config.colors.purple)
.style("transform", "scale(1.05)");
d3.select(this).selectAll("*").style("color", config.colors.white);
}
})
.on("mouseleave", function() {
if (!state.showingFeedback && state.selectedTier !== "hybrid") {
d3.select(this)
.style("background", config.colors.white)
.style("transform", "scale(1)");
d3.select(this).select(".tier-name").style("color", config.colors.purple);
d3.select(this).select(".tier-details").style("color", config.colors.gray);
}
});
hybridBtn.append("div")
.attr("class", "tier-name")
.style("font-size", "18px")
.style("font-weight", "bold")
.style("color", config.colors.purple)
.style("margin-bottom", "5px")
.text("HYBRID");
hybridBtn.append("div")
.attr("class", "tier-details")
.style("font-size", "11px")
.style("color", config.colors.gray)
.html("Split Processing<br>Multi-Tier");
// Feedback Panel
const feedbackPanel = gameArea.append("div")
.attr("class", "feedback-panel")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("display", "none")
.style("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
const feedbackHeader = feedbackPanel.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("margin-bottom", "15px");
const feedbackIcon = feedbackHeader.append("span")
.style("font-size", "24px")
.style("margin-right", "10px");
const feedbackTitle = feedbackHeader.append("span")
.style("font-size", "18px")
.style("font-weight", "bold");
const feedbackText = feedbackPanel.append("p")
.style("color", config.colors.gray)
.style("line-height", "1.6")
.style("margin-bottom", "15px");
// Cost/Latency Impact Display
const impactGrid = feedbackPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "15px")
.style("margin-bottom", "15px");
const costImpact = impactGrid.append("div")
.style("background", config.colors.lightGray)
.style("padding", "10px")
.style("border-radius", "8px")
.style("text-align", "center");
const latencyImpact = impactGrid.append("div")
.style("background", config.colors.lightGray)
.style("padding", "10px")
.style("border-radius", "8px")
.style("text-align", "center");
// Next Button
const nextBtn = feedbackPanel.append("button")
.style("width", "100%")
.style("padding", "15px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "8px")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Next Scenario")
.on("click", nextScenario);
// ---------------------------------------------------------------------------
// DATA FLOW VISUALIZATION (SVG)
// ---------------------------------------------------------------------------
const svgContainer = gameArea.append("div")
.style("margin-bottom", "20px");
const svg = svgContainer.append("svg")
.attr("viewBox", "0 0 800 200")
.attr("width", "100%")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("border", `1px solid ${config.colors.lightGray}`);
// Tier visualization
const tierPositions = {
edge: { x: 150, label: "EDGE" },
fog: { x: 400, label: "FOG" },
cloud: { x: 650, label: "CLOUD" }
};
// Connection lines
svg.append("line")
.attr("x1", 200).attr("y1", 100)
.attr("x2", 350).attr("y2", 100)
.attr("stroke", config.colors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
svg.append("line")
.attr("x1", 450).attr("y1", 100)
.attr("x2", 600).attr("y2", 100)
.attr("stroke", config.colors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Tier nodes
Object.entries(tierPositions).forEach(([tier, pos]) => {
const g = svg.append("g")
.attr("class", `svg-tier-${tier}`)
.attr("transform", `translate(${pos.x}, 100)`);
g.append("circle")
.attr("r", 40)
.attr("fill", config.colors.white)
.attr("stroke", config.tiers[tier]?.color || config.colors.purple)
.attr("stroke-width", 3);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", config.tiers[tier]?.color || config.colors.purple)
.attr("font-weight", "bold")
.attr("font-size", "14px")
.text(pos.label);
});
// Data packet (animated)
const dataPacket = svg.append("g")
.attr("class", "data-packet")
.style("opacity", 0);
dataPacket.append("rect")
.attr("x", -15).attr("y", -10)
.attr("width", 30).attr("height", 20)
.attr("rx", 5)
.attr("fill", config.colors.orange);
dataPacket.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", config.colors.white)
.attr("font-size", "8px")
.attr("font-weight", "bold")
.text("DATA");
// ---------------------------------------------------------------------------
// PROGRESS BAR
// ---------------------------------------------------------------------------
const progressContainer = container.append("div")
.style("background", config.colors.navy)
.style("padding", "15px 20px")
.style("border-radius", "0 0 12px 12px");
const progressLabel = progressContainer.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("color", config.colors.lightGray)
.style("font-size", "12px")
.style("margin-bottom", "8px");
progressLabel.append("span").text("Progress");
const progressPercent = progressLabel.append("span").text("0%");
const progressBar = progressContainer.append("div")
.style("background", config.colors.gray)
.style("height", "8px")
.style("border-radius", "4px")
.style("overflow", "hidden");
const progressFill = progressBar.append("div")
.style("background", config.colors.teal)
.style("height", "100%")
.style("width", "0%")
.style("transition", "width 0.3s");
// ---------------------------------------------------------------------------
// GAME FUNCTIONS
// ---------------------------------------------------------------------------
function getCurrentScenarios() {
return scenarios[`level${state.level}`] || [];
}
function getCurrentScenario() {
const levelScenarios = getCurrentScenarios();
return levelScenarios[state.currentScenarioIndex];
}
function updateDisplay() {
const scenario = getCurrentScenario();
if (!scenario) {
showLevelComplete();
return;
}
// Update level badge
const levelNames = {
1: "Level 1: Fundamentals",
2: "Level 2: Trade-offs",
3: "Level 3: Advanced"
};
levelBadge.text(levelNames[state.level]);
// Show hybrid button only in level 3
hybridBtn.style("display", state.level >= 3 ? "block" : "none");
// Update scenario card
scenarioTitle.text(scenario.title);
scenarioDesc.text(scenario.description);
// Update requirements
reqGrid.html("");
const reqItems = [
{ label: "Latency", value: scenario.requirements.latency, icon: "clock" },
{ label: "Bandwidth", value: scenario.requirements.bandwidth, icon: "network" },
{ label: "Compute", value: scenario.requirements.compute, icon: "cpu" },
{ label: "Storage", value: scenario.requirements.storage, icon: "database" },
{ label: "Connectivity", value: scenario.requirements.connectivity, icon: "wifi" }
];
reqItems.forEach(req => {
const item = reqGrid.append("div")
.style("background", config.colors.lightGray)
.style("padding", "10px")
.style("border-radius", "8px");
item.append("div")
.style("font-size", "10px")
.style("color", config.colors.gray)
.style("text-transform", "uppercase")
.style("margin-bottom", "4px")
.text(req.label);
item.append("div")
.style("font-size", "13px")
.style("color", config.colors.navy)
.style("font-weight", "500")
.text(req.value);
});
// Reset tier buttons
resetTierButtons();
// Hide feedback
feedbackPanel.style("display", "none");
state.showingFeedback = false;
state.selectedTier = null;
// Update score
scoreDisplay.text(`${state.score} / ${state.totalQuestions}`);
const efficiency = state.totalQuestions > 0
? Math.round((state.score / state.totalQuestions) * 100)
: 0;
efficiencyDisplay.text(`Efficiency: ${efficiency}%`);
// Update progress
const totalScenarios = 5 + 5 + 5; // 5 per level
const completed = (state.level - 1) * 5 + state.currentScenarioIndex;
const percent = Math.round((completed / totalScenarios) * 100);
progressPercent.text(`${percent}%`);
progressFill.style("width", `${percent}%`);
// Reset data packet
dataPacket.style("opacity", 0);
resetTierHighlights();
}
function resetTierButtons() {
["edge", "fog", "cloud"].forEach(tier => {
const btn = container.select(`.tier-btn-${tier}`);
btn
.style("background", config.colors.white)
.style("transform", "scale(1)")
.style("pointer-events", "auto")
.style("opacity", "1");
btn.select(".tier-name").style("color", config.tiers[tier].color);
btn.select(".tier-details").style("color", config.colors.gray);
});
hybridBtn
.style("background", config.colors.white)
.style("transform", "scale(1)")
.style("pointer-events", "auto")
.style("opacity", "1");
hybridBtn.select(".tier-name").style("color", config.colors.purple);
hybridBtn.select(".tier-details").style("color", config.colors.gray);
}
function resetTierHighlights() {
["edge", "fog", "cloud"].forEach(tier => {
svg.select(`.svg-tier-${tier} circle`)
.attr("stroke-width", 3)
.attr("fill", config.colors.white);
});
}
function selectTier(tier) {
if (state.showingFeedback) return;
state.selectedTier = tier;
state.showingFeedback = true;
state.totalQuestions++;
const scenario = getCurrentScenario();
// Normalize hybrid answer
let normalizedTier = tier;
if (tier === "hybrid") {
// Check which hybrid type matches
if (scenario.optimal.startsWith("hybrid")) {
normalizedTier = scenario.optimal;
} else {
normalizedTier = "hybrid_edge_cloud"; // default hybrid
}
}
// Check if correct
const isOptimal = normalizedTier === scenario.optimal;
const isAcceptable = scenario.acceptable.includes(normalizedTier) ||
(tier === "hybrid" && scenario.acceptable.some(a => a.startsWith("hybrid")));
if (isOptimal || isAcceptable) {
state.score++;
if (scenario.costImpact) {
const optimalCost = scenario.costImpact[scenario.optimal] || 100;
const actualCost = scenario.costImpact[tier] || scenario.costImpact[normalizedTier] || 100;
state.totalCostSaved += (actualCost - optimalCost);
}
}
// Show feedback
showFeedback(tier, scenario, isOptimal, isAcceptable);
// Animate data flow
animateDataFlow(tier);
// Highlight selected tier
highlightTier(tier, isOptimal || isAcceptable);
// Save answer
state.answers.push({
scenario: scenario.id,
answer: tier,
optimal: scenario.optimal,
correct: isOptimal || isAcceptable
});
}
function showFeedback(tier, scenario, isOptimal, isAcceptable) {
feedbackPanel.style("display", "block");
if (isOptimal) {
feedbackHeader.style("color", config.colors.green);
feedbackIcon.text("*");
feedbackTitle.text("Optimal Choice!");
feedbackPanel.style("border-left", `4px solid ${config.colors.green}`);
} else if (isAcceptable) {
feedbackHeader.style("color", config.colors.yellow);
feedbackIcon.text("~");
feedbackTitle.text("Acceptable Choice");
feedbackPanel.style("border-left", `4px solid ${config.colors.yellow}`);
} else {
feedbackHeader.style("color", config.colors.red);
feedbackIcon.text("X");
feedbackTitle.text("Suboptimal Choice");
feedbackPanel.style("border-left", `4px solid ${config.colors.red}`);
}
// Get feedback text
let feedbackKey = tier;
if (tier === "hybrid") {
feedbackKey = scenario.feedback.hybrid_edge_cloud ? "hybrid_edge_cloud" :
scenario.feedback.hybrid_fog_cloud ? "hybrid_fog_cloud" :
scenario.feedback.hybrid_edge_fog ? "hybrid_edge_fog" : "edge";
}
feedbackText.text(scenario.feedback[feedbackKey] || scenario.feedback[scenario.optimal]);
// Show cost/latency impact
if (scenario.costImpact && scenario.latencyImpact) {
const yourCost = scenario.costImpact[tier] || scenario.costImpact[feedbackKey] || 999;
const optimalCost = scenario.costImpact[scenario.optimal];
const yourLatency = scenario.latencyImpact[tier] || scenario.latencyImpact[feedbackKey] || 999999;
const optimalLatency = scenario.latencyImpact[scenario.optimal];
costImpact.html(`
<div style="font-size: 10px; color: ${config.colors.gray}; margin-bottom: 5px;">MONTHLY COST</div>
<div style="font-size: 20px; font-weight: bold; color: ${yourCost <= optimalCost ? config.colors.green : config.colors.red}">
$${yourCost === 999 ? "N/A" : yourCost}
</div>
<div style="font-size: 10px; color: ${config.colors.gray}">Optimal: $${optimalCost}</div>
`);
latencyImpact.html(`
<div style="font-size: 10px; color: ${config.colors.gray}; margin-bottom: 5px;">LATENCY</div>
<div style="font-size: 20px; font-weight: bold; color: ${yourLatency <= optimalLatency * 2 ? config.colors.green : config.colors.red}">
${yourLatency >= 999999 ? "N/A" : yourLatency < 1000 ? yourLatency + "ms" : Math.round(yourLatency/1000) + "s"}
</div>
<div style="font-size: 10px; color: ${config.colors.gray}">Optimal: ${optimalLatency < 1000 ? optimalLatency + "ms" : Math.round(optimalLatency/1000) + "s"}</div>
`);
}
// Update next button text
const levelScenarios = getCurrentScenarios();
if (state.currentScenarioIndex >= levelScenarios.length - 1) {
if (state.level < 3) {
nextBtn.text("Continue to Level " + (state.level + 1));
} else {
nextBtn.text("See Final Results");
}
} else {
nextBtn.text("Next Scenario");
}
// Disable tier buttons
tierButtonsContainer.selectAll("button")
.style("pointer-events", "none")
.style("opacity", "0.6");
// Highlight correct tier button
const optimalTier = scenario.optimal.startsWith("hybrid") ? "hybrid" : scenario.optimal;
const optimalBtn = container.select(`.tier-btn-${optimalTier}`);
optimalBtn
.style("opacity", "1")
.style("background", config.colors.green + "22")
.style("border-color", config.colors.green);
// Update score display
scoreDisplay.text(`${state.score} / ${state.totalQuestions}`);
const efficiency = Math.round((state.score / state.totalQuestions) * 100);
efficiencyDisplay.text(`Efficiency: ${efficiency}%`);
}
function highlightTier(tier, correct) {
const svgTier = tier === "hybrid" ? "fog" : tier; // Hybrid highlights fog node
const color = correct ? config.colors.green : config.colors.red;
svg.select(`.svg-tier-${svgTier} circle`)
.transition()
.duration(300)
.attr("stroke-width", 6)
.attr("fill", color + "22");
}
function animateDataFlow(tier) {
const startX = 50;
const endPositions = {
edge: 150,
fog: 400,
cloud: 650,
hybrid: 400
};
const endX = endPositions[tier] || 400;
dataPacket
.attr("transform", `translate(${startX}, 100)`)
.style("opacity", 1)
.transition()
.duration(1000)
.ease(d3.easeQuadInOut)
.attr("transform", `translate(${endX}, 100)`);
}
function nextScenario() {
const levelScenarios = getCurrentScenarios();
if (state.currentScenarioIndex >= levelScenarios.length - 1) {
// End of level
if (state.level < 3) {
state.level++;
state.currentScenarioIndex = 0;
updateDisplay();
} else {
showGameComplete();
}
} else {
state.currentScenarioIndex++;
updateDisplay();
}
}
function showLevelComplete() {
if (state.level < 3) {
state.level++;
state.currentScenarioIndex = 0;
updateDisplay();
} else {
showGameComplete();
}
}
function showGameComplete() {
state.gameComplete = true;
const efficiency = Math.round((state.score / state.totalQuestions) * 100);
gameArea.html("");
const resultsCard = gameArea.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "40px")
.style("text-align", "center")
.style("box-shadow", "0 4px 16px rgba(0,0,0,0.1)");
resultsCard.append("h2")
.style("color", config.colors.navy)
.style("margin-bottom", "20px")
.text("Challenge Complete!");
resultsCard.append("div")
.style("font-size", "60px")
.style("font-weight", "bold")
.style("color", efficiency >= 80 ? config.colors.green : efficiency >= 60 ? config.colors.yellow : config.colors.red)
.style("margin-bottom", "10px")
.text(`${efficiency}%`);
resultsCard.append("div")
.style("color", config.colors.gray)
.style("font-size", "18px")
.style("margin-bottom", "30px")
.text(`${state.score} of ${state.totalQuestions} optimal placements`);
// Performance breakdown
const breakdown = resultsCard.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(3, 1fr)")
.style("gap", "20px")
.style("margin-bottom", "30px");
const metrics = [
{ label: "Efficiency Score", value: `${efficiency}%`, color: config.colors.teal },
{ label: "Correct Decisions", value: `${state.score}/${state.totalQuestions}`, color: config.colors.green },
{ label: "Mastery Level", value: efficiency >= 90 ? "Expert" : efficiency >= 70 ? "Proficient" : efficiency >= 50 ? "Developing" : "Novice", color: config.colors.purple }
];
metrics.forEach(m => {
const metricCard = breakdown.append("div")
.style("background", config.colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
metricCard.append("div")
.style("font-size", "24px")
.style("font-weight", "bold")
.style("color", m.color)
.text(m.value);
metricCard.append("div")
.style("font-size", "12px")
.style("color", config.colors.gray)
.text(m.label);
});
// Feedback message
let message = "";
if (efficiency >= 90) {
message = "Excellent! You have mastered edge-fog-cloud architecture decisions. You understand latency requirements, cost trade-offs, and when to use hybrid approaches.";
} else if (efficiency >= 70) {
message = "Great work! You understand the fundamentals of compute placement. Review the hybrid scenarios in Level 3 to further strengthen your skills.";
} else if (efficiency >= 50) {
message = "Good effort! Focus on understanding latency requirements - safety-critical applications (<50ms) must be at the edge, while analytics can tolerate cloud latency.";
} else {
message = "Keep learning! Remember the 50-500-5000 rule: <50ms = Edge, <500ms = Fog, >5000ms = Cloud acceptable. Review the chapter content and try again.";
}
resultsCard.append("p")
.style("color", config.colors.gray)
.style("line-height", "1.6")
.style("margin-bottom", "30px")
.text(message);
// Play again button
resultsCard.append("button")
.style("padding", "15px 30px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "8px")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Play Again")
.on("click", resetGame);
}
function resetGame() {
state = {
level: 1,
currentScenarioIndex: 0,
score: 0,
totalQuestions: 0,
totalCostSaved: 0,
answers: [],
gameComplete: false,
showingFeedback: false,
selectedTier: null
};
// Rebuild game area
gameArea.html("");
// Recreate scenario card
const scenarioCardNew = gameArea.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
scenarioCardNew.append("h3")
.attr("class", "scenario-title")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy);
scenarioCardNew.append("p")
.attr("class", "scenario-desc")
.style("color", config.colors.gray)
.style("line-height", "1.6")
.style("margin-bottom", "15px");
scenarioCardNew.append("div")
.attr("class", "req-grid")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(180px, 1fr))")
.style("gap", "10px")
.style("margin-bottom", "15px");
// Recreate tier buttons
const tierBtnsNew = gameArea.append("div")
.attr("class", "tier-buttons")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "20px")
.style("margin-bottom", "20px")
.style("flex-wrap", "wrap");
// ... (simplified - would need full recreation)
// For simplicity, reload the page
location.reload();
}
// ---------------------------------------------------------------------------
// INITIALIZE GAME
// ---------------------------------------------------------------------------
updateDisplay();
return container.node();
}