mutable gameState = ({
level: 1,
currentHop: 0,
score: 0,
lives: 3,
totalLatency: 0,
packetsLost: 0,
gameActive: false,
gameComplete: false,
showEncapsulation: false,
currentChallenge: null,
challengeAnswer: null,
feedbackMessage: "",
feedbackType: "info",
packetData: {
sourceMAC: "AA:BB:CC:DD:EE:01",
destMAC: "11:22:33:44:55:66",
sourceIP: "192.168.1.100",
destIP: "203.0.113.50",
sourcePort: 49152,
destPort: 443,
protocol: "TCP",
payload: "Temperature: 23.5C"
},
history: []
})
// Level Definitions
levelData = ({
1: {
name: "LAN Routing",
description: "Navigate a packet across a local area network from an IoT sensor to the gateway",
hops: [
{
name: "IoT Sensor",
type: "source",
icon: "🌡️",
ip: "192.168.1.100",
mac: "AA:BB:CC:DD:EE:01",
challenges: []
},
{
name: "Switch A",
type: "switch",
icon: "🔀",
ip: null,
mac: "SW:AA:11:22:33:44",
challenges: [
{
type: "mac_vs_ip",
question: "At Layer 2, which address does this switch use to forward the frame?",
options: ["IP Address (192.168.1.100)", "MAC Address (AA:BB:CC:DD:EE:01)", "Port Number (443)", "Protocol Type (TCP)"],
correct: 1,
feedback: {
correct: "Correct! Switches operate at Layer 2 (Data Link) and use MAC addresses to make forwarding decisions. They maintain a MAC address table that maps MAC addresses to physical ports.",
incorrect: "Switches operate at Layer 2 and use MAC addresses, not IP addresses. IP addresses are used by routers at Layer 3."
},
latency: 1
}
]
},
{
name: "Local Router",
type: "router",
icon: "🔀",
ip: "192.168.1.1",
mac: "RR:BB:22:33:44:55",
challenges: [
{
type: "routing_table",
question: "The packet destination is 203.0.113.50 (outside this LAN). Looking at the routing table, where should this packet go next?",
routingTable: [
{ network: "192.168.1.0/24", nextHop: "Local", interface: "eth0" },
{ network: "10.0.0.0/8", nextHop: "192.168.1.254", interface: "eth1" },
{ network: "0.0.0.0/0", nextHop: "ISP Gateway", interface: "wan0" }
],
options: ["Forward to 192.168.1.254 (eth1)", "Keep local on eth0", "Send to default gateway (ISP)", "Drop the packet"],
correct: 2,
feedback: {
correct: "Correct! 203.0.113.50 doesn't match any specific route, so the router uses the default route (0.0.0.0/0) to forward to the ISP gateway. Default routes handle all traffic without a more specific match.",
incorrect: "203.0.113.50 is not in the 192.168.1.0/24 or 10.0.0.0/8 networks. It matches only the default route (0.0.0.0/0), which sends traffic to the ISP gateway."
},
latency: 2
},
{
type: "nat",
question: "The router performs NAT before forwarding. What happens to the source IP address?",
options: [
"Stays as 192.168.1.100 (private IP)",
"Changes to the router's public IP (e.g., 198.51.100.5)",
"Changes to the destination's IP",
"Gets encrypted and hidden"
],
correct: 1,
feedback: {
correct: "Correct! NAT (Network Address Translation) replaces the private source IP (192.168.1.100) with the router's public IP. The router maintains a translation table to route return traffic back to the correct internal device.",
incorrect: "NAT translates the private IP to the router's public IP so the packet can be routed on the internet. Private IPs like 192.168.x.x are not routable on the public internet."
},
latency: 1
}
]
},
{
name: "Gateway",
type: "destination",
icon: "🌐",
ip: "198.51.100.1",
mac: "GW:CC:33:44:55:66",
challenges: []
}
],
successMessage: "Level 1 Complete! You successfully navigated a packet across a LAN, understanding the difference between MAC and IP addressing, and how NAT enables private networks to communicate with the internet."
},
2: {
name: "WAN Routing",
description: "Route a packet across multiple autonomous systems and handle firewall rules",
hops: [
{
name: "Edge Router",
type: "source",
icon: "🔀",
ip: "198.51.100.1",
mac: "ER:AA:11:22:33:44",
challenges: []
},
{
name: "ISP Core Router",
type: "router",
icon: "🏢",
ip: "203.0.113.1",
mac: "ISP:BB:22:33:44:55",
challenges: [
{
type: "tcp_vs_udp",
question: "This packet carries IoT sensor data that MUST arrive reliably. Which transport protocol should be used?",
options: [
"UDP - Lower latency for real-time data",
"TCP - Guaranteed delivery with acknowledgments",
"ICMP - For network diagnostics",
"ARP - For address resolution"
],
correct: 1,
feedback: {
correct: "Correct! TCP provides reliable delivery through acknowledgments, retransmissions, and flow control. For critical sensor data that must not be lost, TCP ensures delivery even if network conditions cause packet loss.",
incorrect: "When data MUST arrive reliably, TCP is the right choice. It provides acknowledgments, retransmissions, and guaranteed delivery. UDP is faster but doesn't guarantee delivery."
},
latency: 3
}
]
},
{
name: "Peering Point",
type: "router",
icon: "🔗",
ip: "203.0.113.10",
mac: "PP:CC:33:44:55:66",
challenges: [
{
type: "qos",
question: "Network congestion detected! This packet contains critical alarm data. How should QoS prioritize it?",
options: [
"Best Effort (default) - No special treatment",
"Expedited Forwarding (EF) - Highest priority, low latency",
"Assured Forwarding (AF) - Medium priority with drop guarantee",
"Drop the packet to reduce congestion"
],
correct: 1,
feedback: {
correct: "Correct! Critical alarm data should use Expedited Forwarding (EF) for highest priority and lowest latency. EF provides dedicated bandwidth and is ideal for delay-sensitive traffic like alarms and voice.",
incorrect: "Critical alarm data requires highest priority. Expedited Forwarding (EF) provides guaranteed low latency and dedicated bandwidth for time-sensitive traffic."
},
latency: 5
}
]
},
{
name: "Cloud Firewall",
type: "firewall",
icon: "🛡️",
ip: "203.0.113.45",
mac: "FW:DD:44:55:66:77",
challenges: [
{
type: "firewall",
question: "The firewall inspects this packet. Source: 198.51.100.5, Dest: 203.0.113.50:443, Protocol: TCP. Which rule applies?",
firewallRules: [
{ rule: "ALLOW", src: "ANY", dest: "203.0.113.50", port: "443", proto: "TCP", desc: "HTTPS to server" },
{ rule: "ALLOW", src: "ANY", dest: "203.0.113.50", port: "80", proto: "TCP", desc: "HTTP to server" },
{ rule: "DENY", src: "10.0.0.0/8", dest: "ANY", port: "ANY", proto: "ANY", desc: "Block private ranges" },
{ rule: "DENY", src: "ANY", dest: "ANY", port: "ANY", proto: "ANY", desc: "Default deny" }
],
options: [
"ALLOW - Matches rule 1 (HTTPS to server)",
"ALLOW - Matches rule 2 (HTTP to server)",
"DENY - Matches rule 3 (private ranges)",
"DENY - Matches rule 4 (default deny)"
],
correct: 0,
feedback: {
correct: "Correct! The packet matches Rule 1: destination 203.0.113.50 on port 443 using TCP (HTTPS). Firewall rules are evaluated in order, and the first matching rule is applied.",
incorrect: "Firewalls evaluate rules in order. This packet to 203.0.113.50:443/TCP matches Rule 1 (HTTPS to server) first, so it's allowed through."
},
latency: 2
}
]
},
{
name: "Cloud Server",
type: "destination",
icon: "☁️",
ip: "203.0.113.50",
mac: "SV:EE:55:66:77:88",
challenges: []
}
],
successMessage: "Level 2 Complete! You navigated across WAN infrastructure, made correct TCP vs UDP decisions, applied QoS prioritization during congestion, and passed firewall inspection!"
},
3: {
name: "IoT Mesh Routing",
description: "Route through a multi-hop IoT mesh network with battery-constrained nodes",
hops: [
{
name: "Soil Sensor",
type: "source",
icon: "🌱",
ip: "fd00::1:100",
mac: "SS:AA:11:22:33:44",
challenges: []
},
{
name: "Mesh Node A",
type: "mesh",
icon: "📡",
ip: "fd00::1:101",
mac: "MN:BB:22:33:44:55",
challenges: [
{
type: "protocol_layer",
question: "This 6LoWPAN mesh network uses IPv6. At which OSI layer does 6LoWPAN header compression operate?",
options: [
"Layer 1 - Physical",
"Layer 2 - Data Link (adaptation sublayer)",
"Layer 3 - Network",
"Layer 4 - Transport"
],
correct: 1,
feedback: {
correct: "Correct! 6LoWPAN operates as an adaptation layer between Layer 2 (Data Link) and Layer 3 (Network). It compresses IPv6 headers to fit within the small frame sizes of IEEE 802.15.4 (127 bytes max).",
incorrect: "6LoWPAN is an adaptation layer that sits between Data Link (L2) and Network (L3) layers. It compresses 40-byte IPv6 headers down to as few as 2 bytes to fit in constrained wireless frames."
},
latency: 10
}
]
},
{
name: "Mesh Node B",
type: "mesh",
icon: "📡",
ip: "fd00::1:102",
mac: "MN:CC:33:44:55:66",
challenges: [
{
type: "congestion",
question: "Node B detects network congestion. Node C (alternate path) has 20% battery, Node B has 80%. Using RPL routing, which path should be chosen?",
options: [
"Always use Node C - shorter path to destination",
"Use Node B - higher battery preserves network lifetime",
"Broadcast to both - redundancy is best",
"Drop packet - wait for congestion to clear"
],
correct: 1,
feedback: {
correct: "Correct! RPL (Routing Protocol for Low-Power and Lossy Networks) considers node metrics like battery level. Routing through Node B preserves Node C's limited battery, extending overall network lifetime. IoT mesh protocols optimize for network longevity, not just shortest path.",
incorrect: "RPL routing considers energy efficiency and node health. Using the node with more battery (80% vs 20%) preserves overall network lifetime - a critical factor in battery-powered IoT deployments."
},
latency: 15
}
]
},
{
name: "Mesh Node D",
type: "mesh",
icon: "📡",
ip: "fd00::1:104",
mac: "MN:DD:44:55:66:77",
challenges: [
{
type: "failure",
question: "Node failure detected! The primary path through Node E is down. The RPL DODAG shows an alternate path through Node F (2 extra hops, 50ms added latency). What should happen?",
options: [
"Drop the packet - destination unreachable",
"Buffer and wait for Node E to recover",
"Reroute through Node F using RPL repair",
"Flood the packet to all neighbors"
],
correct: 2,
feedback: {
correct: "Correct! RPL performs local repair by rerouting through alternate paths in the DODAG (Destination Oriented Directed Acyclic Graph). Node F provides connectivity even with added latency - maintaining packet delivery is priority.",
incorrect: "RPL supports local repair mechanisms. When a path fails, nodes can find alternate routes through the DODAG structure without dropping packets or flooding the network."
},
latency: 50
}
]
},
{
name: "Border Router",
type: "router",
icon: "🔀",
ip: "fd00::1:1",
mac: "BR:EE:55:66:77:88",
challenges: [
{
type: "encapsulation",
question: "The packet is leaving the 6LoWPAN mesh for a standard IPv6 network. What happens to the packet headers?",
options: [
"6LoWPAN compression is removed, full IPv6 headers restored",
"Additional 6LoWPAN header added for tunneling",
"Packet is fragmented into smaller pieces",
"Headers remain compressed for efficiency"
],
correct: 0,
feedback: {
correct: "Correct! The border router decompresses 6LoWPAN headers back to standard IPv6 format. Compression is only used within the constrained mesh network; external networks expect standard IPv6.",
incorrect: "Border routers perform header decompression when packets exit the 6LoWPAN network. The compressed headers are expanded back to full IPv6 format for standard network compatibility."
},
latency: 5
}
]
},
{
name: "Cloud Gateway",
type: "destination",
icon: "☁️",
ip: "2001:db8::50",
mac: "GW:FF:66:77:88:99",
challenges: []
}
],
successMessage: "Level 3 Complete! You mastered IoT mesh routing with 6LoWPAN, RPL energy-aware routing, failure recovery, and header compression/decompression. You're now a Packet Journey Expert!"
}
})
// Current level data
currentLevel = levelData[gameState.level]
// Get current hop
currentHop = currentLevel.hops[gameState.currentHop]
// Start/Reset Game Functions
function startGame() {
mutable gameState = {
...gameState,
gameActive: true,
gameComplete: false,
currentHop: 0,
score: 0,
lives: 3,
totalLatency: 0,
packetsLost: 0,
currentChallenge: null,
challengeAnswer: null,
feedbackMessage: "Your packet begins its journey! Navigate through each network node by answering questions correctly.",
feedbackType: "info",
history: []
}
}
function resetGame() {
mutable gameState = {
level: 1,
currentHop: 0,
score: 0,
lives: 3,
totalLatency: 0,
packetsLost: 0,
gameActive: false,
gameComplete: false,
showEncapsulation: false,
currentChallenge: null,
challengeAnswer: null,
feedbackMessage: "",
feedbackType: "info",
packetData: {
sourceMAC: "AA:BB:CC:DD:EE:01",
destMAC: "11:22:33:44:55:66",
sourceIP: "192.168.1.100",
destIP: "203.0.113.50",
sourcePort: 49152,
destPort: 443,
protocol: "TCP",
payload: "Temperature: 23.5C"
},
history: []
}
}
function nextLevel() {
if (gameState.level < 3) {
mutable gameState = {
...gameState,
level: gameState.level + 1,
currentHop: 0,
currentChallenge: null,
challengeAnswer: null,
feedbackMessage: `Starting Level ${gameState.level + 1}: ${levelData[gameState.level + 1].name}`,
feedbackType: "info",
history: []
}
} else {
mutable gameState = {
...gameState,
gameComplete: true,
feedbackMessage: "Congratulations! You've completed all three levels of Packet Journey!",
feedbackType: "success"
}
}
}
// Process current hop - check for challenges
function processHop() {
const hop = currentLevel.hops[gameState.currentHop]
if (hop.challenges && hop.challenges.length > 0 && gameState.currentChallenge === null) {
// Start first challenge at this hop
mutable gameState = {
...gameState,
currentChallenge: 0,
feedbackMessage: `You've reached ${hop.name}. Answer the challenge to proceed.`,
feedbackType: "info"
}
} else if (!hop.challenges || hop.challenges.length === 0 || gameState.currentChallenge >= hop.challenges.length) {
// No challenges or all completed - move to next hop
advanceHop()
}
}
function advanceHop() {
const hop = currentLevel.hops[gameState.currentHop]
const newHistory = [...gameState.history, { hop: hop.name, icon: hop.icon }]
if (gameState.currentHop < currentLevel.hops.length - 1) {
mutable gameState = {
...gameState,
currentHop: gameState.currentHop + 1,
currentChallenge: null,
challengeAnswer: null,
history: newHistory,
feedbackMessage: `Packet forwarded to next hop.`,
feedbackType: "info"
}
} else {
// Level complete
mutable gameState = {
...gameState,
history: newHistory,
feedbackMessage: currentLevel.successMessage,
feedbackType: "success"
}
}
}
// Answer submission
function submitAnswer(answerIndex) {
const challenge = currentHop.challenges[gameState.currentChallenge]
const isCorrect = answerIndex === challenge.correct
if (isCorrect) {
const newScore = gameState.score + (100 * gameState.level)
const newLatency = gameState.totalLatency + challenge.latency
mutable gameState = {
...gameState,
score: newScore,
totalLatency: newLatency,
challengeAnswer: answerIndex,
feedbackMessage: challenge.feedback.correct,
feedbackType: "success"
}
// Move to next challenge or advance
setTimeout(() => {
if (gameState.currentChallenge < currentHop.challenges.length - 1) {
mutable gameState = {
...gameState,
currentChallenge: gameState.currentChallenge + 1,
challengeAnswer: null,
feedbackMessage: "Next challenge at this hop...",
feedbackType: "info"
}
} else {
advanceHop()
}
}, 2000)
} else {
const newLives = gameState.lives - 1
const newPacketsLost = gameState.packetsLost + 1
if (newLives <= 0) {
mutable gameState = {
...gameState,
lives: 0,
packetsLost: newPacketsLost,
gameActive: false,
feedbackMessage: "Packet lost! All retries exhausted. Click 'Restart Level' to try again.",
feedbackType: "error"
}
} else {
mutable gameState = {
...gameState,
lives: newLives,
packetsLost: newPacketsLost,
challengeAnswer: answerIndex,
feedbackMessage: challenge.feedback.incorrect + ` (${newLives} retries remaining)`,
feedbackType: "error"
}
}
}
}
// UI Rendering
viewof gameAction = {
const container = html`<div style="font-family: system-ui, -apple-system, sans-serif;"></div>`
// Header
const header = html`
<div style="background: linear-gradient(135deg, #2C3E50 0%, #16A085 100%); color: white; padding: 20px; border-radius: 12px 12px 0 0; margin-bottom: 0;">
<h3 style="margin: 0 0 8px 0; font-size: 1.5em;">Packet Journey Adventure</h3>
<p style="margin: 0; opacity: 0.9;">${!gameState.gameActive ? "Guide your packet safely to its destination!" : `Level ${gameState.level}: ${currentLevel.name}`}</p>
</div>
`
container.appendChild(header)
// Stats bar
if (gameState.gameActive) {
const stats = html`
<div style="background: #34495E; color: white; padding: 12px 20px; display: flex; justify-content: space-around; flex-wrap: wrap; gap: 10px;">
<span>Score: <strong>${gameState.score}</strong></span>
<span>Lives: <strong style="color: ${gameState.lives <= 1 ? '#E74C3C' : '#2ECC71'};">${'❤️'.repeat(gameState.lives)}${'🖤'.repeat(3 - gameState.lives)}</strong></span>
<span>Latency: <strong>${gameState.totalLatency}ms</strong></span>
<span>Packets Lost: <strong style="color: ${gameState.packetsLost > 0 ? '#E74C3C' : '#2ECC71'};">${gameState.packetsLost}</strong></span>
</div>
`
container.appendChild(stats)
}
// Main game area
const mainArea = html`<div style="background: #ECF0F1; padding: 20px; min-height: 400px;"></div>`
if (!gameState.gameActive && !gameState.gameComplete) {
// Start screen
const startScreen = html`
<div style="text-align: center; padding: 40px;">
<div style="font-size: 4em; margin-bottom: 20px;">📦➡️🌐</div>
<h3 style="color: #2C3E50; margin-bottom: 15px;">Welcome to Packet Journey!</h3>
<p style="color: #7F8C8D; margin-bottom: 20px; max-width: 500px; margin-left: auto; margin-right: auto;">
Experience networking concepts by guiding a data packet through networks.
Make routing decisions, configure protocols, and overcome network challenges.
</p>
<div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; text-align: left; max-width: 400px; margin-left: auto; margin-right: auto;">
<h4 style="color: #2C3E50; margin-top: 0;">Three Levels:</h4>
<ul style="color: #34495E; margin: 0; padding-left: 20px;">
<li><strong>Level 1:</strong> LAN Routing - MAC vs IP, NAT</li>
<li><strong>Level 2:</strong> WAN Routing - TCP/UDP, QoS, Firewalls</li>
<li><strong>Level 3:</strong> IoT Mesh - 6LoWPAN, RPL, Mesh Routing</li>
</ul>
</div>
<button onclick=${() => startGame()} style="background: #16A085; color: white; border: none; padding: 15px 40px; font-size: 1.2em; border-radius: 8px; cursor: pointer; transition: background 0.3s;">
Start Journey
</button>
</div>
`
mainArea.appendChild(startScreen)
} else if (gameState.gameComplete) {
// Victory screen
const victoryScreen = html`
<div style="text-align: center; padding: 40px;">
<div style="font-size: 4em; margin-bottom: 20px;">🏆</div>
<h3 style="color: #16A085; margin-bottom: 15px;">Congratulations!</h3>
<p style="color: #2C3E50; margin-bottom: 20px;">You've mastered all three levels of Packet Journey!</p>
<div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: inline-block; text-align: left;">
<h4 style="color: #2C3E50; margin-top: 0;">Final Statistics:</h4>
<table style="color: #34495E;">
<tr><td style="padding-right: 20px;">Total Score:</td><td><strong>${gameState.score}</strong></td></tr>
<tr><td>Total Latency:</td><td><strong>${gameState.totalLatency}ms</strong></td></tr>
<tr><td>Packets Lost:</td><td><strong>${gameState.packetsLost}</strong></td></tr>
<tr><td>Lives Remaining:</td><td><strong>${gameState.lives}</strong></td></tr>
</table>
</div>
<br/>
<button onclick=${() => resetGame()} style="background: #2C3E50; color: white; border: none; padding: 12px 30px; font-size: 1em; border-radius: 8px; cursor: pointer;">
Play Again
</button>
</div>
`
mainArea.appendChild(victoryScreen)
} else {
// Active game
// Network visualization
const networkViz = html`
<div style="background: white; padding: 15px; border-radius: 8px; margin-bottom: 20px; overflow-x: auto;">
<div style="display: flex; align-items: center; justify-content: center; gap: 5px; min-width: max-content;">
${currentLevel.hops.map((hop, i) => {
const isVisited = i < gameState.currentHop
const isCurrent = i === gameState.currentHop
const isPending = i > gameState.currentHop
const nodeStyle = `
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
border-radius: 8px;
min-width: 80px;
background: ${isCurrent ? '#16A085' : isVisited ? '#2ECC71' : '#BDC3C7'};
color: ${isCurrent || isVisited ? 'white' : '#7F8C8D'};
transition: all 0.3s;
${isCurrent ? 'box-shadow: 0 0 15px rgba(22, 160, 133, 0.5); transform: scale(1.1);' : ''}
`
const node = html`
<div style="${nodeStyle}">
<span style="font-size: 2em;">${hop.icon}</span>
<span style="font-size: 0.75em; font-weight: bold; text-align: center;">${hop.name}</span>
${hop.ip ? html`<span style="font-size: 0.6em; opacity: 0.8;">${hop.ip}</span>` : ''}
</div>
`
if (i < currentLevel.hops.length - 1) {
const arrow = html`<span style="font-size: 1.5em; color: ${isVisited ? '#2ECC71' : '#BDC3C7'};">→</span>`
return html`${node}${arrow}`
}
return node
})}
</div>
</div>
`
mainArea.appendChild(networkViz)
// Challenge area
if (gameState.currentChallenge !== null && currentHop.challenges && currentHop.challenges[gameState.currentChallenge]) {
const challenge = currentHop.challenges[gameState.currentChallenge]
const challengeArea = html`
<div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h4 style="color: #2C3E50; margin-top: 0;">
<span style="background: #E67E22; color: white; padding: 3px 8px; border-radius: 4px; font-size: 0.8em; margin-right: 8px;">${challenge.type.replace(/_/g, ' ').toUpperCase()}</span>
Challenge at ${currentHop.name}
</h4>
${challenge.routingTable ? html`
<div style="background: #2C3E50; color: #2ECC71; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 0.85em; margin-bottom: 15px; overflow-x: auto;">
<div style="color: #E67E22; margin-bottom: 5px;">Routing Table:</div>
<table style="width: 100%; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #34495E;">
<th style="text-align: left; padding: 4px 8px; color: #3498DB;">Network</th>
<th style="text-align: left; padding: 4px 8px; color: #3498DB;">Next Hop</th>
<th style="text-align: left; padding: 4px 8px; color: #3498DB;">Interface</th>
</tr>
${challenge.routingTable.map(r => html`
<tr>
<td style="padding: 4px 8px;">${r.network}</td>
<td style="padding: 4px 8px;">${r.nextHop}</td>
<td style="padding: 4px 8px;">${r.interface}</td>
</tr>
`)}
</table>
</div>
` : ''}
${challenge.firewallRules ? html`
<div style="background: #2C3E50; color: #2ECC71; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 0.8em; margin-bottom: 15px; overflow-x: auto;">
<div style="color: #E67E22; margin-bottom: 5px;">Firewall Rules (evaluated in order):</div>
<table style="width: 100%; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #34495E;">
<th style="text-align: left; padding: 4px; color: #3498DB;">#</th>
<th style="text-align: left; padding: 4px; color: #3498DB;">Action</th>
<th style="text-align: left; padding: 4px; color: #3498DB;">Src</th>
<th style="text-align: left; padding: 4px; color: #3498DB;">Dest</th>
<th style="text-align: left; padding: 4px; color: #3498DB;">Port</th>
</tr>
${challenge.firewallRules.map((r, i) => html`
<tr>
<td style="padding: 4px; color: ${r.rule === 'ALLOW' ? '#2ECC71' : '#E74C3C'};">${i + 1}</td>
<td style="padding: 4px; color: ${r.rule === 'ALLOW' ? '#2ECC71' : '#E74C3C'};">${r.rule}</td>
<td style="padding: 4px;">${r.src}</td>
<td style="padding: 4px;">${r.dest}</td>
<td style="padding: 4px;">${r.port}</td>
</tr>
`)}
</table>
</div>
` : ''}
<p style="color: #34495E; margin-bottom: 15px; font-weight: 500;">${challenge.question}</p>
<div style="display: flex; flex-direction: column; gap: 10px;">
${challenge.options.map((opt, i) => {
const isSelected = gameState.challengeAnswer === i
const isCorrect = i === challenge.correct
const showResult = gameState.challengeAnswer !== null
let btnStyle = `
padding: 12px 15px;
border: 2px solid;
border-radius: 8px;
cursor: ${showResult ? 'default' : 'pointer'};
text-align: left;
font-size: 0.95em;
transition: all 0.3s;
`
if (showResult) {
if (isCorrect) {
btnStyle += `background: #D5F5E3; border-color: #2ECC71; color: #1E8449;`
} else if (isSelected && !isCorrect) {
btnStyle += `background: #FADBD8; border-color: #E74C3C; color: #922B21;`
} else {
btnStyle += `background: #F8F9F9; border-color: #BDC3C7; color: #7F8C8D;`
}
} else {
btnStyle += `background: white; border-color: #3498DB; color: #2C3E50;`
}
return html`
<button
onclick=${() => !showResult && submitAnswer(i)}
style="${btnStyle}"
disabled=${showResult}
>
${String.fromCharCode(65 + i)}. ${opt}
${showResult && isCorrect ? ' ✓' : ''}
${showResult && isSelected && !isCorrect ? ' ✗' : ''}
</button>
`
})}
</div>
</div>
`
mainArea.appendChild(challengeArea)
} else if (currentHop.type === 'destination' || currentHop.type === 'source') {
// At source or destination with no challenges
if (currentHop.type === 'source' && gameState.currentHop === 0) {
const startHop = html`
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<p style="color: #2C3E50; margin-bottom: 15px;">Your packet is ready to begin its journey from <strong>${currentHop.name}</strong>.</p>
<button onclick=${() => advanceHop()} style="background: #16A085; color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-size: 1em;">
Begin Transmission →
</button>
</div>
`
mainArea.appendChild(startHop)
} else if (currentHop.type === 'destination') {
const levelComplete = html`
<div style="background: white; padding: 20px; border-radius: 8px; text-align: center;">
<div style="font-size: 3em; margin-bottom: 15px;">🎉</div>
<h4 style="color: #16A085; margin-bottom: 10px;">Packet Delivered Successfully!</h4>
<p style="color: #34495E; margin-bottom: 20px;">${currentLevel.successMessage}</p>
${gameState.level < 3 ? html`
<button onclick=${() => nextLevel()} style="background: #16A085; color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-size: 1em;">
Continue to Level ${gameState.level + 1} →
</button>
` : html`
<button onclick=${() => { mutable gameState = {...gameState, gameComplete: true} }} style="background: #E67E22; color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-size: 1em;">
View Final Results 🏆
</button>
`}
</div>
`
mainArea.appendChild(levelComplete)
}
}
// Feedback area
if (gameState.feedbackMessage) {
const feedbackColors = {
success: { bg: '#D5F5E3', border: '#2ECC71', text: '#1E8449' },
error: { bg: '#FADBD8', border: '#E74C3C', text: '#922B21' },
info: { bg: '#D6EAF8', border: '#3498DB', text: '#1A5276' }
}
const colors = feedbackColors[gameState.feedbackType]
const feedback = html`
<div style="background: ${colors.bg}; border: 2px solid ${colors.border}; color: ${colors.text}; padding: 15px; border-radius: 8px; margin-top: 15px;">
<strong>${gameState.feedbackType === 'success' ? '✓' : gameState.feedbackType === 'error' ? '✗' : 'ℹ'}</strong> ${gameState.feedbackMessage}
</div>
`
mainArea.appendChild(feedback)
}
// Packet encapsulation viewer (toggleable)
const encapToggle = html`
<div style="margin-top: 20px;">
<button onclick=${() => { mutable gameState = {...gameState, showEncapsulation: !gameState.showEncapsulation} }}
style="background: #34495E; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 0.9em;">
${gameState.showEncapsulation ? 'Hide' : 'Show'} Packet Encapsulation 📦
</button>
</div>
`
mainArea.appendChild(encapToggle)
if (gameState.showEncapsulation) {
const encapView = html`
<div style="background: #2C3E50; padding: 15px; border-radius: 8px; margin-top: 10px; color: white; font-family: monospace; font-size: 0.85em; overflow-x: auto;">
<div style="color: #E67E22; margin-bottom: 10px; font-weight: bold;">Current Packet Structure (OSI Layers):</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
<div style="background: #9B59B6; padding: 8px 12px; border-radius: 4px;">
<strong>Layer 7 (Application):</strong> Payload: "${gameState.packetData.payload}"
</div>
<div style="background: #3498DB; padding: 8px 12px; border-radius: 4px;">
<strong>Layer 4 (Transport):</strong> ${gameState.packetData.protocol} | Src Port: ${gameState.packetData.sourcePort} | Dst Port: ${gameState.packetData.destPort}
</div>
<div style="background: #E67E22; padding: 8px 12px; border-radius: 4px;">
<strong>Layer 3 (Network):</strong> Src IP: ${gameState.packetData.sourceIP} | Dst IP: ${gameState.packetData.destIP}
</div>
<div style="background: #E74C3C; padding: 8px 12px; border-radius: 4px;">
<strong>Layer 2 (Data Link):</strong> Src MAC: ${gameState.packetData.sourceMAC} | Dst MAC: ${currentHop.mac || 'TBD'}
</div>
<div style="background: #7F8C8D; padding: 8px 12px; border-radius: 4px;">
<strong>Layer 1 (Physical):</strong> Transmitted as radio waves / electrical signals
</div>
</div>
<div style="margin-top: 10px; color: #BDC3C7; font-size: 0.9em;">
Note: Headers are added (encapsulation) when sending and removed (decapsulation) when receiving at each layer.
</div>
</div>
`
mainArea.appendChild(encapView)
}
// Restart button if game over
if (!gameState.gameActive && gameState.lives <= 0) {
const restart = html`
<div style="text-align: center; margin-top: 20px;">
<button onclick=${() => startGame()} style="background: #E74C3C; color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-size: 1em;">
Restart Level
</button>
</div>
`
mainArea.appendChild(restart)
}
}
container.appendChild(mainArea)
// Footer
const footer = html`
<div style="background: #2C3E50; color: white; padding: 15px 20px; border-radius: 0 0 12px 12px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<span style="opacity: 0.8; font-size: 0.9em;">Learn networking by doing!</span>
${gameState.gameActive ? html`
<button onclick=${() => resetGame()} style="background: #7F8C8D; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 0.85em;">
Reset Game
</button>
` : ''}
</div>
`
container.appendChild(footer)
return container
}