Build a Browser-Based Hacking Sim Game
Build an animated terminal hacking game with missions using HTML, CSS, and JS.
Introduction
30 Second Summary
Every hacking scene in movies shows a dark terminal with glowing green text and rapid-fire commands. That same thrill of "breaking in" can be turned into a game that teaches real cybersecurity concepts.
In this project, you will build a browser-based hacking simulation game called NetHack Terminal where players type commands to complete network reconnaissance and password cracking missions.
What You'll Build
A fully playable terminal-style game with animated boot sequences, an interactive command parser, two progressive missions, and persistent progress saving.
By the end of this project, you'll have:
- An animated terminal interface with CRT scanlines, glowing text, and a blinking cursor built with pure CSS.
- An interactive command system that parses player input and generates contextual responses using JavaScript DOM manipulation.
- Two playable missions teaching network reconnaissance and password cracking through simulated cybersecurity tools.
- Secret Mission: Add a third mission with a firewall bypass challenge and an ASCII network map visualization.
Are there any prerequisites?
None. This project runs entirely in the browser using HTML, CSS, and JavaScript. You do not need to install any packages, frameworks, or build tools.
Before We Start
Before We Start...
Set Up Your Project Files
Every web project starts with a home folder and a set of files that work together.
Your hacking game needs three files: an HTML file for the page structure, a CSS file for the dark terminal visuals, and a JavaScript file for the game logic.
In this step, get ready to:
- Create a project folder with three core files.
- Write the HTML boilerplate that links everything together.
- Verify the page loads in your browser without errors.
Create the project folder and files
Before writing any code, you need a dedicated folder on your Desktop to keep all your game files organized.
- In VS Code, click Terminal in the top menu bar.
- Click New Terminal.
What is the terminal?
The terminal is a text-based interface where you type commands to create folders, files, and run programs. VS Code has one built in so you never have to leave your editor.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir nethack-terminal
cd nethack-terminal
- Create the three project files by running this command:
macOS
touch index.html style.css game.js
Windows
New-Item index.html, style.css, game.js
- Confirm the files exist by running:
ls
You should see index.html, style.css, and game.js in the output.
- Now open this folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the nethack-terminal folder, and click Open.
Don't see your files in the terminal?
Make sure you ran cd ~/Desktop before mkdir. If ls shows nothing, your terminal might be in the wrong folder. Run pwd to check your current location.
Help me figure out what went wrong with my terminal commands
Write the HTML boilerplate
HTML is the skeleton of every web page. This boilerplate file tells the browser what fonts to load, which CSS file to use for styling, and which JavaScript file runs the game logic.
- In the VS Code Explorer sidebar on the left, click index.html to open it.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetHack Terminal</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="game.js"></script>
</body>
</html>
What does this code do?
The <meta charset="UTF-8"> tag ensures special characters display correctly. The viewport meta tag makes the page responsive on different screen sizes.
The first <link> tag loads JetBrains Mono from Google Fonts. This is the monospace font that gives your terminal its authentic hacker aesthetic. The second <link> connects your CSS stylesheet.
The <script> tag at the bottom of <body> loads your game logic after the page structure is ready.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing red squiggly lines in VS Code?
Make sure you pasted the code into the index.html file (check the tab name at the top of the editor). If you accidentally pasted into style.css or game.js, undo with Cmd+Z (macOS) or Ctrl+Z (Windows) and paste into the correct file.
Help me figure out what went wrong with my HTML
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetHack Terminal</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="game.js"></script>
</body>
</html>
Verify the page loads without errors
Opening your HTML file in a browser confirms everything is wired up correctly. An error-free console means the browser found your CSS and JavaScript files.
- Open the nethack-terminal folder on your Desktop in Finder (macOS) or File Explorer (Windows).
- Double-click index.html to open it in your default browser.
You should see a completely blank white page. This is expected because your CSS and JavaScript files are still empty.
- Open the browser's developer console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
- Check that the console area shows no red error messages.
What should I see?
The console should be empty or show only a blinking cursor. If you see red text mentioning a file not found, it means one of your linked files (style.css or game.js) is missing or misnamed.
Seeing a file not found error?
This means the browser cannot find one of the linked files. Check that style.css and game.js exist in the same folder as index.html. File names are case-sensitive, so make sure there are no capital letters or extra spaces.
Help me troubleshoot my file not found error
Your project skeleton is ready. Next up, you will build the dark terminal interface with glowing green text, scanlines, and a blinking cursor.
Build the Terminal Interface and Animations
Your project files are in place and the blank page loads without errors. Now it is time to build the visual interface.
A hacking simulation needs to feel authentic. In this step, you will create a terminal interface with a dark CRT-style aesthetic, glowing green text, scanline overlays, and a blinking cursor.
In this step, get ready to:
- Create the HTML structure for the terminal container, header, output area, and input line.
- Write CSS for the dark terminal aesthetic with green phosphor text, CRT scanlines, and text glow.
- Add keyframe animations for the blinking cursor, pulsing status dot, and typing effect.
Create the terminal HTML structure
Every terminal has three zones: a header bar showing status, a scrollable body for text output, and an input line where the user types commands.
- In index.html, find the <body> tag. Replace everything between <body> and </body> (including the existing script tag) with the following code:
<div class="scanlines"></div>
<div class="terminal">
<div class="terminal-header">
<div class="terminal-title">NETHACK TERMINAL v1.0</div>
<div class="terminal-status">
<span class="status-dot status-active"></span>
<span class="status-text">CONNECTED</span>
</div>
</div>
<div class="terminal-body" id="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">user@nethack:~$</span>
<input type="text" id="terminal-input" class="terminal-input" autofocus autocomplete="off" spellcheck="false">
<span class="cursor"></span>
</div>
</div>
<script src="game.js"></script>
What does this code do?
The .scanlines div will create a full-screen CRT overlay effect. The .terminal div is the main container holding the entire interface.
Inside the terminal, .terminal-header shows the app title and a status indicator. The .terminal-body is where game output text will appear. The .terminal-input-line holds the green prompt, a text input field, and a blinking cursor.
- Save index.html.
- Refresh your browser. You should see unstyled text including "NETHACK TERMINAL v1.0", "CONNECTED", and the prompt user@nethack:~$ with a text input field.
Don't see any text on the page?
Make sure the <script src="game.js"></script> tag is still present at the bottom, just before </body>. Also check that you did not accidentally delete the <body> or </body> tags themselves.
Open your browser's developer console (right-click the page, select Inspect, then the Console tab) to check for any red error messages.
Help me debug my terminal HTML structure.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NetHack Terminal</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="scanlines"></div>
<div class="terminal">
<div class="terminal-header">
<div class="terminal-title">NETHACK TERMINAL v1.0</div>
<div class="terminal-status">
<span class="status-dot status-active"></span>
<span class="status-text">CONNECTED</span>
</div>
</div>
<div class="terminal-body" id="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">user@nethack:~$</span>
<input type="text" id="terminal-input" class="terminal-input" autofocus autocomplete="off" spellcheck="false">
<span class="cursor"></span>
</div>
</div>
<script src="game.js"></script>
</body>
</html>
Style the terminal with CSS
The terminal needs a dark, immersive look. You will build up the CSS in layers: first the page background and CRT scanlines, then the terminal container and header, then the input area, and finally the text color system.
- In style.css, add the base page styles and scanline overlay by pasting this code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #0a0a0f;
font-family: 'JetBrains Mono', monospace;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scanlines {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0, 255, 65, 0.03) 2px,
transparent 3px
);
}
What does this code do?
The * selector resets all margins and padding so the browser defaults do not interfere. The body gets a near-black background and centers everything with flexbox.
The .scanlines div uses a repeating-linear-gradient to draw faint green horizontal lines every 3 pixels across the entire screen. This recreates the CRT monitor scanline effect. The pointer-events: none ensures it does not block clicks.
- Save style.css and refresh your browser.
- The page background is now near-black. If you look closely, you can see faint horizontal lines across the screen.
Still seeing a white background?
Check that your index.html has <link rel="stylesheet" href="style.css"> in the <head>. Also confirm that style.css is in the same folder as index.html.
Help me figure out why my CSS is not loading.
- Next, add the terminal container and header styles below the .scanlines block:
.terminal {
width: 90vw;
max-width: 900px;
height: 85vh;
background-color: #0d1117;
border: 1px solid #1a3a1a;
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow: 0 0 20px rgba(0, 255, 65, 0.1), inset 0 0 60px rgba(0, 0, 0, 0.5);
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background-color: #161b22;
border-bottom: 1px solid #1a3a1a;
}
.terminal-title {
color: #00ff41;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 0 8px rgba(0, 255, 65, 0.5);
}
.terminal-status {
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #00ff41;
animation: pulse-glow 2s ease-in-out infinite;
}
.status-text {
color: #4a8a4a;
font-size: 10px;
letter-spacing: 1px;
}
What does this code do?
The .terminal container is a tall dark box centered on the page. The box-shadow gives it a subtle green outer glow and a dark inner shadow for depth.
The header shows the title in green with a text-shadow glow effect. The .status-dot is a small green circle that will pulse once you add the keyframe animation later.
- Save style.css and refresh your browser.
- You should see a dark terminal box centered on the page with a green "NETHACK TERMINAL v1.0" title in the header and a small green dot on the right.
Terminal box not visible?
Make sure every opening curly brace { has a matching closing brace }. A missing brace will break all styles that follow it. Also check that every property line ends with a semicolon.
Help me debug why my terminal container is not showing up.
- Add the terminal body and input area styles below the .status-text block:
.terminal-body {
flex: 1;
overflow-y: auto;
padding: 16px;
scroll-behavior: smooth;
}
.terminal-body::-webkit-scrollbar {
width: 6px;
}
.terminal-body::-webkit-scrollbar-track {
background: #0d1117;
}
.terminal-body::-webkit-scrollbar-thumb {
background: #1a3a1a;
border-radius: 3px;
}
.terminal-input-line {
display: flex;
align-items: center;
padding: 12px 16px;
border-top: 1px solid #1a3a1a;
background-color: #0d1117;
}
.prompt {
color: #00ff41;
font-size: 14px;
margin-right: 8px;
white-space: nowrap;
text-shadow: 0 0 5px rgba(0, 255, 65, 0.4);
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #e0e0e0;
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
caret-color: transparent;
}
.cursor {
display: inline-block;
width: 8px;
height: 16px;
background-color: #00ff41;
animation: blink 1s step-end infinite;
}
What does this code do?
The .terminal-body expands to fill the space between the header and input. It scrolls vertically when text overflows. The custom scrollbar styles keep the dark aesthetic.
The .terminal-input is invisible (transparent background, no border) so only the typed text shows. The caret-color: transparent hides the browser's default text cursor because you are replacing it with the green .cursor block.
- Save style.css and refresh your browser.
- The terminal now has a green user@nethack:~$ prompt at the bottom with a green cursor block next to it. The cursor block will not blink yet because the keyframe animation has not been added.
Prompt or cursor not visible?
If you see the prompt text but no green block, confirm that the .cursor span exists in your HTML (inside .terminal-input-line, after the <input> tag). If neither shows, check that .terminal-input-line has display: flex in your CSS.
Help me fix my terminal input area styling.
- Add the output line text styles below the .cursor block:
.output-line {
margin-bottom: 4px;
font-size: 13px;
line-height: 1.6;
color: #b0b0b0;
}
.output-line.command {
color: #e0e0e0;
}
.output-line.system {
color: #00ff41;
text-shadow: 0 0 5px rgba(0, 255, 65, 0.3);
}
.output-line.error {
color: #ff4444;
text-shadow: 0 0 5px rgba(255, 68, 68, 0.3);
}
.output-line.warning {
color: #ffb800;
}
.output-line.success {
color: #00ff41;
font-weight: 700;
text-shadow: 0 0 8px rgba(0, 255, 65, 0.5);
}
.output-line.info {
color: #7ec8e3;
}
.output-line.highlight {
color: #00ff41;
border-left: 2px solid #00ff41;
padding-left: 8px;
}
What does this code do?
These classes define a color system for different types of terminal output. Green (.system, .success) signals positive events, red (.error) flags problems, yellow (.warning) shows hints, and blue (.info) displays informational text.
The JavaScript game engine will apply these classes dynamically when printing text to the terminal in the next step.
- Save style.css.
- You will not see a visible change yet. These classes will style the text output that JavaScript generates in the next step.
Add the animation keyframes
CSS keyframe animations bring the terminal to life. You need four animations: a typing effect for the boot sequence, a blink for the cursor, a pulse-glow for the status dot, and a fade-in for new output lines.
- Add the animation keyframes and utility classes at the bottom of style.css:
.typing-line {
overflow: hidden;
white-space: nowrap;
animation: typing 0.5s steps(40) forwards;
width: 0;
}
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 4px rgba(0, 255, 65, 0.6); }
50% { box-shadow: 0 0 12px rgba(0, 255, 65, 1); }
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fade-in 0.3s ease-out forwards;
}
What do these animations do?
The blink keyframe toggles the cursor between visible and invisible every second using step-end timing for a sharp on/off effect (no fading).
The pulse-glow keyframe makes the status dot's green glow expand and contract. The typing keyframe animates an element's width from 0 to 100% to simulate text being typed character by character. The fade-in keyframe slides new lines up slightly while fading them in.
- Save style.css and refresh your browser.
- The green cursor block now blinks on and off. The small status dot in the header pulses with a subtle glow that expands and contracts.
Cursor not blinking?
Make sure the @keyframes blink block is present in your CSS. The name inside @keyframes must exactly match the name in animation: blink 1s step-end infinite on the .cursor class.
Help me debug why my cursor animation is not working.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #0a0a0f;
font-family: 'JetBrains Mono', monospace;
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scanlines {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0, 255, 65, 0.03) 2px,
transparent 3px
);
}
.terminal {
width: 90vw;
max-width: 900px;
height: 85vh;
background-color: #0d1117;
border: 1px solid #1a3a1a;
border-radius: 4px;
display: flex;
flex-direction: column;
box-shadow: 0 0 20px rgba(0, 255, 65, 0.1), inset 0 0 60px rgba(0, 0, 0, 0.5);
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background-color: #161b22;
border-bottom: 1px solid #1a3a1a;
}
.terminal-title {
color: #00ff41;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 0 8px rgba(0, 255, 65, 0.5);
}
.terminal-status {
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #00ff41;
animation: pulse-glow 2s ease-in-out infinite;
}
.status-text {
color: #4a8a4a;
font-size: 10px;
letter-spacing: 1px;
}
.terminal-body {
flex: 1;
overflow-y: auto;
padding: 16px;
scroll-behavior: smooth;
}
.terminal-body::-webkit-scrollbar {
width: 6px;
}
.terminal-body::-webkit-scrollbar-track {
background: #0d1117;
}
.terminal-body::-webkit-scrollbar-thumb {
background: #1a3a1a;
border-radius: 3px;
}
.terminal-input-line {
display: flex;
align-items: center;
padding: 12px 16px;
border-top: 1px solid #1a3a1a;
background-color: #0d1117;
}
.prompt {
color: #00ff41;
font-size: 14px;
margin-right: 8px;
white-space: nowrap;
text-shadow: 0 0 5px rgba(0, 255, 65, 0.4);
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: #e0e0e0;
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
caret-color: transparent;
}
.cursor {
display: inline-block;
width: 8px;
height: 16px;
background-color: #00ff41;
animation: blink 1s step-end infinite;
}
.output-line {
margin-bottom: 4px;
font-size: 13px;
line-height: 1.6;
color: #b0b0b0;
}
.output-line.command {
color: #e0e0e0;
}
.output-line.system {
color: #00ff41;
text-shadow: 0 0 5px rgba(0, 255, 65, 0.3);
}
.output-line.error {
color: #ff4444;
text-shadow: 0 0 5px rgba(255, 68, 68, 0.3);
}
.output-line.warning {
color: #ffb800;
}
.output-line.success {
color: #00ff41;
font-weight: 700;
text-shadow: 0 0 8px rgba(0, 255, 65, 0.5);
}
.output-line.info {
color: #7ec8e3;
}
.output-line.highlight {
color: #00ff41;
border-left: 2px solid #00ff41;
padding-left: 8px;
}
.typing-line {
overflow: hidden;
white-space: nowrap;
animation: typing 0.5s steps(40) forwards;
width: 0;
}
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 4px rgba(0, 255, 65, 0.6); }
50% { box-shadow: 0 0 12px rgba(0, 255, 65, 1); }
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fade-in 0.3s ease-out forwards;
}
Your terminal looks the part. Next up, you will bring it to life with JavaScript by adding a boot sequence, a command parser, and the game engine that makes the whole thing interactive.
Build the Command System and Game Engine
Your terminal looks great, but it does not do anything yet. Right now, typing into the input field has no effect because game.js is empty.
Every game needs an engine. In this step, you will build the core system that powers everything: a game state that tracks progress, a command parser that understands player input, and a boot sequence that makes the terminal feel alive.
In this step, get ready to:
- Create the game state, network data, and utility functions.
- Implement the command parser that matches typed commands to actions.
- Wire up keyboard input so the terminal responds to the player.
Create the game state and world data
Before you can process commands, you need the data those commands will act on. This includes the player's progress, the simulated network of hosts, and the file system the player will explore.
- In VS Code, open game.js (currently empty) and paste the following game data:
const output = document.getElementById('terminal-output');
const input = document.getElementById('terminal-input');
// Track the player's progress through missions
const gameState = {
currentMission: 1,
missionComplete: { 1: false, 2: false },
discoveredHosts: [],
connectedHost: null,
hashCaptured: false,
passwordCracked: false
};
// The simulated network of hosts the player will discover
const networkHosts = [
{ ip: '192.168.1.1', name: 'gateway-router', ports: [22, 80, 443], os: 'RouterOS' },
{ ip: '192.168.1.10', name: 'workstation-01', ports: [22, 3389], os: 'Linux' },
{ ip: '192.168.1.25', name: 'file-server', ports: [21, 22, 445, 8080], os: 'Ubuntu 22.04' },
{ ip: '192.168.1.50', name: 'target-db', ports: [22, 3306, 5432], os: 'Debian 12' }
];
// Virtual file system the player explores after connecting
const fileSystem = {
'target-db': {
'/home': ['admin', 'backups'],
'/home/admin': ['.bash_history', 'notes.txt'],
'/home/backups': ['shadow.bak', 'readme.txt'],
files: {
'notes.txt': 'Reminder: update DB credentials. Current hash stored in /home/backups/shadow.bak',
'shadow.bak': 'admin:$6$rounds=5000$saltsalt$kH3rqWJzPG5x7VdYfOzKj2xR9mN4bT6wL1cA8pQ3dF==',
'readme.txt': 'Backup of shadow file. DO NOT share externally.',
'.bash_history': 'mysql -u admin -p\ncat /etc/shadow\ncp /etc/shadow /home/backups/shadow.bak'
}
}
};
// Navigation and command history tracking
let currentDir = '~';
let commandHistory = [];
let historyIndex = -1;
What does this code do?
This defines the entire game world. gameState tracks what the player has accomplished so far (which mission they are on, which hosts they have found, whether they have cracked the password).
The networkHosts array holds four simulated servers the player will discover. Each has an IP address, open ports, and an operating system. The fileSystem object mimics a real Linux file structure with directories and files containing clues.
- Add the terminal output utility functions below the code you just pasted:
function printLine(text, className = '') {
// Create a new line element and add it to the terminal output
const line = document.createElement('div');
line.className = 'output-line fade-in ' + className;
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function printLines(lines, className = '', delay = 80) {
// Print multiple lines with a delay between each
let i = 0;
function next() {
if (i < lines.length) {
printLine(lines[i], className);
i++;
setTimeout(next, delay);
}
}
next();
}
What does this code do?
These are the building blocks for all terminal output. printLine() creates a new <div> element, applies CSS classes for styling (like system, error, success), and auto-scrolls to the bottom.
The printLines() function prints an array of text with timed delays, creating the typing effect you see in movie-style terminals.
- Add the boot sequence function below the utility functions:
function bootSequence() {
const bootLines = [
'[BOOT] Initializing NetHack Terminal v1.0...',
'[BOOT] Loading kernel modules... OK',
'[BOOT] Establishing encrypted connection... OK',
'[BOOT] Network interface: eth0 UP',
'[BOOT] IP assigned: 10.0.0.77',
'[BOOT] Firewall rules loaded.',
'[BOOT] System ready.',
'',
'================================================',
' NETHACK TERMINAL - Ethical Hacking Simulator',
' For educational purposes only.',
'================================================',
'',
'Welcome, Operator. You have 2 missions to complete.',
'Type "help" to see available commands.',
'Type "mission" to see your current objective.',
''
];
// Disable input during boot animation
input.disabled = true;
let i = 0;
function nextLine() {
if (i < bootLines.length) {
const className = bootLines[i].startsWith('[BOOT]') ? 'system' : '';
printLine(bootLines[i], className);
i++;
// Boot lines get a longer delay for dramatic effect
const delay = bootLines[i - 1].startsWith('[BOOT]') ? 150 : 60;
setTimeout(nextLine, delay);
} else {
// Re-enable input once boot is complete
input.disabled = false;
input.focus();
}
}
nextLine();
}
What does this code do?
The boot sequence creates the dramatic startup animation. It disables the input field so the player cannot type during the animation, then prints each line with a delay. Lines starting with [BOOT] get the green system CSS class and a slower 150ms delay.
Once all lines are printed, the input is re-enabled and focused so the player can start typing commands.
- Save game.js.
Seeing errors in the browser console?
If you see a message about Cannot read properties of null, make sure the id values in your getElementById calls match your HTML exactly: terminal-output and terminal-input.
help me debug a null reference error in my game.js file
Implement the command parser
Now you need functions that respond when the player types a command. The parser will read what the player typed, figure out which command it matches, and display the right output.
- Add the showHelp() function below bootSequence():
function showHelp() {
const helpLines = [
'Available commands:',
'',
' help - Show this help message',
' mission - Show current mission objectives',
' scan - Scan the network for hosts',
' connect - Connect to a discovered host (usage: connect <ip>)',
' ls - List files in current directory',
' cat - Read a file (usage: cat <filename>)',
' crack - Attempt to crack a captured hash',
' clear - Clear the terminal',
' status - Show mission progress',
''
];
helpLines.forEach(line => printLine(line, 'info'));
}
What does this code do?
This prints a formatted list of every command available in the game. Each line uses the info CSS class which renders in light blue, making the help text visually distinct from other output.
- Add the showMission() function below showHelp():
function showMission() {
if (gameState.currentMission === 1 && !gameState.missionComplete[1]) {
printLine('MISSION 1: Network Reconnaissance', 'highlight');
printLine('', '');
printLine('Objective: Discover all hosts on the network and', '');
printLine('connect to the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Use "scan" to discover network hosts', gameState.discoveredHosts.length > 0 ? 'success' : '');
printLine(' 2. Identify the target database server', gameState.discoveredHosts.length >= 4 ? 'success' : '');
printLine(' 3. Use "connect <ip>" to access the target', gameState.connectedHost === 'target-db' ? 'success' : '');
} else if (gameState.currentMission === 2 && !gameState.missionComplete[2]) {
printLine('MISSION 2: Password Cracking', 'highlight');
printLine('', '');
printLine('Objective: Find and crack the admin password hash', '');
printLine('on the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Explore the file system with "ls" and "cat"', gameState.hashCaptured ? 'success' : '');
printLine(' 2. Locate the password hash file', gameState.hashCaptured ? 'success' : '');
printLine(' 3. Use "crack" to run a dictionary attack', gameState.passwordCracked ? 'success' : '');
} else if (gameState.missionComplete[1] && gameState.missionComplete[2]) {
printLine('ALL MISSIONS COMPLETE!', 'success');
printLine('Congratulations, Operator. You have proven your skills.', 'system');
}
}
What does this code do?
This function checks the player's current mission and shows the relevant objectives. Completed steps turn green (the success class) so the player can see their progress at a glance.
The conditional structure means the display adapts as the player progresses. Mission 2 only shows after Mission 1 is complete, and a final congratulation appears once both are done.
- Add the save/load and command processing functions below showMission():
function saveProgress() {
localStorage.setItem('nethack-save', JSON.stringify(gameState));
}
function loadProgress() {
const saved = localStorage.getItem('nethack-save');
if (saved) {
const data = JSON.parse(saved);
Object.assign(gameState, data);
}
}
What does this code do?
These two functions handle persistence using the browser's localStorage API. When the player completes an objective, saveProgress() serializes the game state to JSON and stores it under the key nethack-save.
When the page loads, loadProgress() checks for saved data and restores it. This means the player can close the browser and pick up where they left off.
- Add the processCommand() function below loadProgress():
function processCommand(cmd) {
const trimmed = cmd.trim();
if (!trimmed) return;
// Store command in history for arrow key navigation
commandHistory.push(trimmed);
historyIndex = commandHistory.length;
// Echo the command back to the terminal
printLine('user@nethack:~$ ' + trimmed, 'command');
// Split into command name and arguments
const parts = trimmed.split(' ');
const command = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
// Route to the correct handler
switch (command) {
case 'help':
showHelp();
break;
case 'mission':
showMission();
break;
case 'scan':
break;
case 'connect':
break;
case 'ls':
break;
case 'cat':
break;
case 'crack':
break;
case 'clear':
output.innerHTML = '';
break;
case 'status':
break;
default:
printLine('Command not found: ' + command, 'error');
printLine('Type "help" for available commands.', '');
break;
}
}
What does this code do?
This is the brain of the game. It takes the raw text the player typed, splits it into a command name and arguments, then uses a switch statement to route to the right handler function.
Right now, scan, connect, ls, cat, crack, and status are empty stubs. You will implement them in the next steps. The help, mission, and clear commands already work.
- Save game.js.
Seeing 'Unexpected token' or syntax errors?
Check that every opening brace { has a matching closing brace }. The switch statement inside processCommand is deeply nested, so a missing brace is the most common issue.
help me find a syntax error in my processCommand function
Wire up the input field to handle keyboard events
The command parser exists, but nothing calls it yet. You need an event listener that fires when the player presses Enter, sends their input to processCommand(), and clears the input field for the next command.
- Add the event listeners and initialization calls at the very bottom of game.js:
// Listen for keyboard input
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
processCommand(input.value);
input.value = '';
} else if (e.key === 'ArrowUp') {
// Navigate backward through command history
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
// Navigate forward through command history
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
// Clicking anywhere on the page focuses the input
document.addEventListener('click', function() {
input.focus();
});
// Load any saved progress and start the boot animation
loadProgress();
bootSequence();
What does this code do?
The keydown listener handles three keys. Enter sends the current input to the parser and clears the field. ArrowUp and ArrowDown cycle through previously typed commands, just like a real terminal.
The click listener ensures the input stays focused even if the player clicks on the output area. Finally, loadProgress() restores any saved state and bootSequence() kicks off the startup animation.
- Save game.js and refresh your browser.
- You should see the boot sequence animation play. Green [BOOT] lines appear one by one, followed by the welcome message.
- After the boot finishes, type help and press Enter. A blue list of available commands should appear.
- Type mission and press Enter. You should see Mission 1 objectives displayed with a green left border.
Nothing happens when you type?
Open the browser console (press F12 then click **Console**). Look for red error messages. The most common issues are a typo in an element ID or a missing function closing brace.
If the boot animation plays but typing does nothing, check that input.disabled = false is reached at the end of the bootSequence() function.
help me figure out why my terminal input is not responding to keyboard events
✔️ Awesome, I've got everything!
Great. Double check you have saved game.js.
ⓧ I'd like to double check the full code
Here is the complete game.js file at this point:
const output = document.getElementById('terminal-output');
const input = document.getElementById('terminal-input');
const gameState = {
currentMission: 1,
missionComplete: { 1: false, 2: false },
discoveredHosts: [],
connectedHost: null,
hashCaptured: false,
passwordCracked: false
};
const networkHosts = [
{ ip: '192.168.1.1', name: 'gateway-router', ports: [22, 80, 443], os: 'RouterOS' },
{ ip: '192.168.1.10', name: 'workstation-01', ports: [22, 3389], os: 'Linux' },
{ ip: '192.168.1.25', name: 'file-server', ports: [21, 22, 445, 8080], os: 'Ubuntu 22.04' },
{ ip: '192.168.1.50', name: 'target-db', ports: [22, 3306, 5432], os: 'Debian 12' }
];
const fileSystem = {
'target-db': {
'/home': ['admin', 'backups'],
'/home/admin': ['.bash_history', 'notes.txt'],
'/home/backups': ['shadow.bak', 'readme.txt'],
files: {
'notes.txt': 'Reminder: update DB credentials. Current hash stored in /home/backups/shadow.bak',
'shadow.bak': 'admin:$6$rounds=5000$saltsalt$kH3rqWJzPG5x7VdYfOzKj2xR9mN4bT6wL1cA8pQ3dF==',
'readme.txt': 'Backup of shadow file. DO NOT share externally.',
'.bash_history': 'mysql -u admin -p\ncat /etc/shadow\ncp /etc/shadow /home/backups/shadow.bak'
}
}
};
let currentDir = '~';
let commandHistory = [];
let historyIndex = -1;
function printLine(text, className = '') {
const line = document.createElement('div');
line.className = 'output-line fade-in ' + className;
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function printLines(lines, className = '', delay = 80) {
let i = 0;
function next() {
if (i < lines.length) {
printLine(lines[i], className);
i++;
setTimeout(next, delay);
}
}
next();
}
function bootSequence() {
const bootLines = [
'[BOOT] Initializing NetHack Terminal v1.0...',
'[BOOT] Loading kernel modules... OK',
'[BOOT] Establishing encrypted connection... OK',
'[BOOT] Network interface: eth0 UP',
'[BOOT] IP assigned: 10.0.0.77',
'[BOOT] Firewall rules loaded.',
'[BOOT] System ready.',
'',
'================================================',
' NETHACK TERMINAL - Ethical Hacking Simulator',
' For educational purposes only.',
'================================================',
'',
'Welcome, Operator. You have 2 missions to complete.',
'Type "help" to see available commands.',
'Type "mission" to see your current objective.',
''
];
input.disabled = true;
let i = 0;
function nextLine() {
if (i < bootLines.length) {
const className = bootLines[i].startsWith('[BOOT]') ? 'system' : '';
printLine(bootLines[i], className);
i++;
const delay = bootLines[i - 1].startsWith('[BOOT]') ? 150 : 60;
setTimeout(nextLine, delay);
} else {
input.disabled = false;
input.focus();
}
}
nextLine();
}
function showHelp() {
const helpLines = [
'Available commands:',
'',
' help - Show this help message',
' mission - Show current mission objectives',
' scan - Scan the network for hosts',
' connect - Connect to a discovered host (usage: connect <ip>)',
' ls - List files in current directory',
' cat - Read a file (usage: cat <filename>)',
' crack - Attempt to crack a captured hash',
' clear - Clear the terminal',
' status - Show mission progress',
''
];
helpLines.forEach(line => printLine(line, 'info'));
}
function showMission() {
if (gameState.currentMission === 1 && !gameState.missionComplete[1]) {
printLine('MISSION 1: Network Reconnaissance', 'highlight');
printLine('', '');
printLine('Objective: Discover all hosts on the network and', '');
printLine('connect to the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Use "scan" to discover network hosts', gameState.discoveredHosts.length > 0 ? 'success' : '');
printLine(' 2. Identify the target database server', gameState.discoveredHosts.length >= 4 ? 'success' : '');
printLine(' 3. Use "connect <ip>" to access the target', gameState.connectedHost === 'target-db' ? 'success' : '');
} else if (gameState.currentMission === 2 && !gameState.missionComplete[2]) {
printLine('MISSION 2: Password Cracking', 'highlight');
printLine('', '');
printLine('Objective: Find and crack the admin password hash', '');
printLine('on the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Explore the file system with "ls" and "cat"', gameState.hashCaptured ? 'success' : '');
printLine(' 2. Locate the password hash file', gameState.hashCaptured ? 'success' : '');
printLine(' 3. Use "crack" to run a dictionary attack', gameState.passwordCracked ? 'success' : '');
} else if (gameState.missionComplete[1] && gameState.missionComplete[2]) {
printLine('ALL MISSIONS COMPLETE!', 'success');
printLine('Congratulations, Operator. You have proven your skills.', 'system');
}
}
function saveProgress() {
localStorage.setItem('nethack-save', JSON.stringify(gameState));
}
function loadProgress() {
const saved = localStorage.getItem('nethack-save');
if (saved) {
const data = JSON.parse(saved);
Object.assign(gameState, data);
}
}
function processCommand(cmd) {
const trimmed = cmd.trim();
if (!trimmed) return;
commandHistory.push(trimmed);
historyIndex = commandHistory.length;
printLine('user@nethack:~$ ' + trimmed, 'command');
const parts = trimmed.split(' ');
const command = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
switch (command) {
case 'help':
showHelp();
break;
case 'mission':
showMission();
break;
case 'scan':
break;
case 'connect':
break;
case 'ls':
break;
case 'cat':
break;
case 'crack':
break;
case 'clear':
output.innerHTML = '';
break;
case 'status':
break;
default:
printLine('Command not found: ' + command, 'error');
printLine('Type "help" for available commands.', '');
break;
}
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
processCommand(input.value);
input.value = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
document.addEventListener('click', function() {
input.focus();
});
loadProgress();
bootSequence();
Your game engine is alive. The terminal boots up, accepts input, and responds to help and mission commands. Next up, you will implement the scan and connect commands so the player can actually explore the network and complete Mission 1.
Build Mission 1 (Network Reconnaissance)
Your terminal looks great and accepts commands, but typing scan or connect does nothing yet. The switch cases are empty stubs waiting for real game logic.
In this step, you will build the first playable mission. The player scans a simulated network to discover hosts, connects to a target database server, and triggers a mission-complete event that unlocks Mission 2.
In this step, get ready to:
- Implement the scan command with animated host discovery.
- Implement the connect command to access discovered hosts.
- Add mission objective tracking that detects completion and unlocks Mission 2.
Implement the scan command
The scan command simulates a network reconnaissance tool. It loops through the predefined hosts and reveals each one with a 600ms delay, creating an animated effect that feels like a real scan running in the background.
- In game.js, find the closing } of the showMission() function.
- Add the scanNetwork() function directly below it by copying this code:
function scanNetwork() {
// Prevent running the scan again if all hosts are already found
if (gameState.discoveredHosts.length >= 4) {
printLine('Network scan already complete. All hosts discovered.', 'warning');
return;
}
printLine('Initiating network scan on 192.168.1.0/24...', 'system');
printLine('', '');
// Reveal each host one at a time with a delay
let i = 0;
function revealHost() {
if (i < networkHosts.length) {
const host = networkHosts[i];
setTimeout(() => {
printLine(' [+] Host discovered: ' + host.ip, 'success');
printLine(' Name: ' + host.name, '');
printLine(' Ports: ' + host.ports.join(', '), '');
printLine(' OS: ' + host.os, '');
printLine('', '');
gameState.discoveredHosts.push(host);
// After the last host, print summary and check mission
if (i === networkHosts.length - 1) {
printLine('Scan complete. ' + networkHosts.length + ' hosts found.', 'system');
printLine('Hint: The target database server looks interesting...', 'warning');
checkMission1();
}
i++;
revealHost();
}, 600);
}
}
revealHost();
}
What does this code do?
The guard clause at the top prevents the player from running the scan multiple times. If all 4 hosts are already in discoveredHosts, it prints a warning and exits early.
The revealHost() inner function uses recursion with setTimeout to reveal one host every 600 milliseconds. Each host's IP address, name, ports, and OS are printed to the terminal. After the last host, it calls checkMission1() to test whether mission conditions are met.
You won't see a change yet. The scanNetwork() function exists but nothing calls it. You will wire it into the command system in the final substep.
Implement the connect command
Once the player discovers hosts, they need a way to connect to one. The connect command validates the IP, checks whether the host was already discovered, then simulates an SSH connection with a short delay.
- Directly below the scanNetwork() function you just added, paste the connectToHost() function:
function connectToHost(ip) {
// Show usage hint if no IP was provided
if (!ip) {
printLine('Usage: connect <ip>', 'error');
printLine('Example: connect 192.168.1.50', '');
return;
}
// Check if the host was discovered by the scan
const host = gameState.discoveredHosts.find(h => h.ip === ip);
if (!host) {
printLine('Error: Host ' + ip + ' not found.', 'error');
printLine('Run "scan" first to discover hosts.', 'warning');
return;
}
// Simulate SSH connection with a short delay
printLine('Connecting to ' + ip + ' (' + host.name + ')...', 'system');
setTimeout(() => {
printLine('[+] Connection established via SSH (port 22)', 'success');
printLine(' Welcome to ' + host.name + ' (' + host.os + ')', '');
printLine('', '');
gameState.connectedHost = host.name;
currentDir = '/home';
checkMission1();
}, 500);
}
What does this code do?
The function takes an ip argument from whatever the player types after connect. Two validation checks run first: one for a missing IP, and one to confirm the host was found during a scan.
If both checks pass, setTimeout simulates a 500ms connection delay. It then updates gameState.connectedHost to the host's name and sets currentDir to /home so the file system commands know where they are. Finally, it calls checkMission1() to test for completion.
You still won't see a change. The next substep adds the mission-tracking logic and wires all three commands into the game.
Add mission objective tracking and wire up the commands
Both scanNetwork() and connectToHost() call a function named checkMission1() that does not exist yet. You also need a showStatus() function so the player can review their progress at any time.
- Directly below the connectToHost() function, add both checkMission1() and showStatus() by pasting this code:
function checkMission1() {
// Mission 1 is complete when the player has scanned all hosts AND connected to target-db
if (gameState.connectedHost === 'target-db' && gameState.discoveredHosts.length >= 4 && !gameState.missionComplete[1]) {
setTimeout(() => {
printLine('', '');
printLine('========================================', 'success');
printLine(' MISSION 1 COMPLETE: Reconnaissance', 'success');
printLine('========================================', 'success');
printLine('', '');
printLine('Mission 2 unlocked: Password Cracking', 'system');
printLine('Type "mission" to see your new objective.', '');
printLine('', '');
gameState.missionComplete[1] = true;
gameState.currentMission = 2;
saveProgress();
}, 300);
}
}
function showStatus() {
printLine('--- OPERATOR STATUS ---', 'info');
printLine('', '');
printLine(' Mission 1 (Recon): ' + (gameState.missionComplete[1] ? 'COMPLETE' : 'IN PROGRESS'), gameState.missionComplete[1] ? 'success' : 'warning');
printLine(' Mission 2 (Cracking): ' + (gameState.missionComplete[2] ? 'COMPLETE' : gameState.currentMission >= 2 ? 'IN PROGRESS' : 'LOCKED'), gameState.missionComplete[2] ? 'success' : 'warning');
printLine('', '');
printLine(' Hosts discovered: ' + gameState.discoveredHosts.length + '/4', '');
printLine(' Connected to: ' + (gameState.connectedHost || 'none'), '');
printLine(' Hash captured: ' + (gameState.hashCaptured ? 'Yes' : 'No'), '');
printLine(' Password cracked: ' + (gameState.passwordCracked ? 'Yes' : 'No'), '');
printLine('', '');
}
What does this code do?
checkMission1() checks two conditions: the player must have discovered all 4 hosts AND connected to target-db. When both are true, it displays a success banner, flips missionComplete[1] to true, advances currentMission to 2, and saves to localStorage.
showStatus() prints a dashboard of all game progress. It uses ternary operators to show COMPLETE, IN PROGRESS, or LOCKED for each mission based on the current game state.
The functions exist now, but the command system still has empty stubs. Time to wire everything together.
- Scroll down to the processCommand() function and find these three stub cases inside the switch block:
case 'scan':
break;
case 'connect':
break;
- Replace those two cases with the following, so they call the functions you just created:
case 'scan':
scanNetwork();
break;
case 'connect':
connectToHost(args);
break;
- Now find the status stub case further down:
case 'status':
break;
- Replace it with:
case 'status':
showStatus();
break;
- Save game.js and refresh your browser.
- Type scan into the terminal and press Enter. You should see four hosts appear one by one with green text showing their IP addresses, names, ports, and operating systems.
- After the scan finishes, type connect 192.168.1.50 and press Enter. You should see a green "MISSION 1 COMPLETE" banner appear.
- Type status to confirm Mission 1 shows COMPLETE and Mission 2 shows IN PROGRESS.
Scan runs but no mission complete banner?
Make sure you run scan first and wait for all 4 hosts to appear before typing connect 192.168.1.50. Both conditions must be true for mission completion.
If you see a "checkMission1 is not defined" error in the browser console, check that the checkMission1() function is placed before saveProgress() in your file. If your game has leftover save data from earlier testing, open the browser console and run localStorage.clear() then refresh the page.
Why isn't my Mission 1 completing after I scan and connect?
Why recursive setTimeout instead of setInterval?
The revealHost() function calls itself at the end of each timeout rather than using setInterval. This guarantees each host finishes printing before the next one starts. With setInterval, you would need to manually clear the interval and manage an external counter.
✔️ Awesome, I've got everything!
Great. Double check you have saved game.js before moving on.
ⓧ I'd like to double check the full code
Here is the complete game.js file at this point. Compare your code against it:
const output = document.getElementById('terminal-output');
const input = document.getElementById('terminal-input');
const gameState = {
currentMission: 1,
missionComplete: { 1: false, 2: false },
discoveredHosts: [],
connectedHost: null,
hashCaptured: false,
passwordCracked: false
};
const networkHosts = [
{ ip: '192.168.1.1', name: 'gateway-router', ports: [22, 80, 443], os: 'RouterOS' },
{ ip: '192.168.1.10', name: 'workstation-01', ports: [22, 3389], os: 'Linux' },
{ ip: '192.168.1.25', name: 'file-server', ports: [21, 22, 445, 8080], os: 'Ubuntu 22.04' },
{ ip: '192.168.1.50', name: 'target-db', ports: [22, 3306, 5432], os: 'Debian 12' }
];
const fileSystem = {
'target-db': {
'/home': ['admin', 'backups'],
'/home/admin': ['.bash_history', 'notes.txt'],
'/home/backups': ['shadow.bak', 'readme.txt'],
files: {
'notes.txt': 'Reminder: update DB credentials. Current hash stored in /home/backups/shadow.bak',
'shadow.bak': 'admin:$6$rounds=5000$saltsalt$kH3rqWJzPG5x7VdYfOzKj2xR9mN4bT6wL1cA8pQ3dF==',
'readme.txt': 'Backup of shadow file. DO NOT share externally.',
'.bash_history': 'mysql -u admin -p\ncat /etc/shadow\ncp /etc/shadow /home/backups/shadow.bak'
}
}
};
let currentDir = '~';
let commandHistory = [];
let historyIndex = -1;
function printLine(text, className = '') {
const line = document.createElement('div');
line.className = 'output-line fade-in ' + className;
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function printLines(lines, className = '', delay = 80) {
let i = 0;
function next() {
if (i < lines.length) {
printLine(lines[i], className);
i++;
setTimeout(next, delay);
}
}
next();
}
function bootSequence() {
const bootLines = [
'[BOOT] Initializing NetHack Terminal v1.0...',
'[BOOT] Loading kernel modules... OK',
'[BOOT] Establishing encrypted connection... OK',
'[BOOT] Network interface: eth0 UP',
'[BOOT] IP assigned: 10.0.0.77',
'[BOOT] Firewall rules loaded.',
'[BOOT] System ready.',
'',
'================================================',
' NETHACK TERMINAL - Ethical Hacking Simulator',
' For educational purposes only.',
'================================================',
'',
'Welcome, Operator. You have 2 missions to complete.',
'Type "help" to see available commands.',
'Type "mission" to see your current objective.',
''
];
input.disabled = true;
let i = 0;
function nextLine() {
if (i < bootLines.length) {
const className = bootLines[i].startsWith('[BOOT]') ? 'system' : '';
printLine(bootLines[i], className);
i++;
const delay = bootLines[i - 1].startsWith('[BOOT]') ? 150 : 60;
setTimeout(nextLine, delay);
} else {
input.disabled = false;
input.focus();
}
}
nextLine();
}
function showHelp() {
const helpLines = [
'Available commands:',
'',
' help - Show this help message',
' mission - Show current mission objectives',
' scan - Scan the network for hosts',
' connect - Connect to a discovered host (usage: connect <ip>)',
' ls - List files in current directory',
' cat - Read a file (usage: cat <filename>)',
' crack - Attempt to crack a captured hash',
' clear - Clear the terminal',
' status - Show mission progress',
''
];
helpLines.forEach(line => printLine(line, 'info'));
}
function showMission() {
if (gameState.currentMission === 1 && !gameState.missionComplete[1]) {
printLine('MISSION 1: Network Reconnaissance', 'highlight');
printLine('', '');
printLine('Objective: Discover all hosts on the network and', '');
printLine('connect to the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Use "scan" to discover network hosts', gameState.discoveredHosts.length > 0 ? 'success' : '');
printLine(' 2. Identify the target database server', gameState.discoveredHosts.length >= 4 ? 'success' : '');
printLine(' 3. Use "connect <ip>" to access the target', gameState.connectedHost === 'target-db' ? 'success' : '');
} else if (gameState.currentMission === 2 && !gameState.missionComplete[2]) {
printLine('MISSION 2: Password Cracking', 'highlight');
printLine('', '');
printLine('Objective: Find and crack the admin password hash', '');
printLine('on the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Explore the file system with "ls" and "cat"', gameState.hashCaptured ? 'success' : '');
printLine(' 2. Locate the password hash file', gameState.hashCaptured ? 'success' : '');
printLine(' 3. Use "crack" to run a dictionary attack', gameState.passwordCracked ? 'success' : '');
} else if (gameState.missionComplete[1] && gameState.missionComplete[2]) {
printLine('ALL MISSIONS COMPLETE!', 'success');
printLine('Congratulations, Operator. You have proven your skills.', 'system');
}
}
function scanNetwork() {
if (gameState.discoveredHosts.length >= 4) {
printLine('Network scan already complete. All hosts discovered.', 'warning');
return;
}
printLine('Initiating network scan on 192.168.1.0/24...', 'system');
printLine('', '');
let i = 0;
function revealHost() {
if (i < networkHosts.length) {
const host = networkHosts[i];
setTimeout(() => {
printLine(' [+] Host discovered: ' + host.ip, 'success');
printLine(' Name: ' + host.name, '');
printLine(' Ports: ' + host.ports.join(', '), '');
printLine(' OS: ' + host.os, '');
printLine('', '');
gameState.discoveredHosts.push(host);
if (i === networkHosts.length - 1) {
printLine('Scan complete. ' + networkHosts.length + ' hosts found.', 'system');
printLine('Hint: The target database server looks interesting...', 'warning');
checkMission1();
}
i++;
revealHost();
}, 600);
}
}
revealHost();
}
function connectToHost(ip) {
if (!ip) {
printLine('Usage: connect <ip>', 'error');
printLine('Example: connect 192.168.1.50', '');
return;
}
const host = gameState.discoveredHosts.find(h => h.ip === ip);
if (!host) {
printLine('Error: Host ' + ip + ' not found.', 'error');
printLine('Run "scan" first to discover hosts.', 'warning');
return;
}
printLine('Connecting to ' + ip + ' (' + host.name + ')...', 'system');
setTimeout(() => {
printLine('[+] Connection established via SSH (port 22)', 'success');
printLine(' Welcome to ' + host.name + ' (' + host.os + ')', '');
printLine('', '');
gameState.connectedHost = host.name;
currentDir = '/home';
checkMission1();
}, 500);
}
function checkMission1() {
if (gameState.connectedHost === 'target-db' && gameState.discoveredHosts.length >= 4 && !gameState.missionComplete[1]) {
setTimeout(() => {
printLine('', '');
printLine('========================================', 'success');
printLine(' MISSION 1 COMPLETE: Reconnaissance', 'success');
printLine('========================================', 'success');
printLine('', '');
printLine('Mission 2 unlocked: Password Cracking', 'system');
printLine('Type "mission" to see your new objective.', '');
printLine('', '');
gameState.missionComplete[1] = true;
gameState.currentMission = 2;
saveProgress();
}, 300);
}
}
function showStatus() {
printLine('--- OPERATOR STATUS ---', 'info');
printLine('', '');
printLine(' Mission 1 (Recon): ' + (gameState.missionComplete[1] ? 'COMPLETE' : 'IN PROGRESS'), gameState.missionComplete[1] ? 'success' : 'warning');
printLine(' Mission 2 (Cracking): ' + (gameState.missionComplete[2] ? 'COMPLETE' : gameState.currentMission >= 2 ? 'IN PROGRESS' : 'LOCKED'), gameState.missionComplete[2] ? 'success' : 'warning');
printLine('', '');
printLine(' Hosts discovered: ' + gameState.discoveredHosts.length + '/4', '');
printLine(' Connected to: ' + (gameState.connectedHost || 'none'), '');
printLine(' Hash captured: ' + (gameState.hashCaptured ? 'Yes' : 'No'), '');
printLine(' Password cracked: ' + (gameState.passwordCracked ? 'Yes' : 'No'), '');
printLine('', '');
}
function saveProgress() {
localStorage.setItem('nethack-save', JSON.stringify(gameState));
}
function loadProgress() {
const saved = localStorage.getItem('nethack-save');
if (saved) {
const data = JSON.parse(saved);
Object.assign(gameState, data);
}
}
function processCommand(cmd) {
const trimmed = cmd.trim();
if (!trimmed) return;
commandHistory.push(trimmed);
historyIndex = commandHistory.length;
printLine('user@nethack:~$ ' + trimmed, 'command');
const parts = trimmed.split(' ');
const command = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
switch (command) {
case 'help':
showHelp();
break;
case 'mission':
showMission();
break;
case 'scan':
scanNetwork();
break;
case 'connect':
connectToHost(args);
break;
case 'ls':
break;
case 'cat':
break;
case 'crack':
break;
case 'clear':
output.innerHTML = '';
break;
case 'status':
showStatus();
break;
default:
printLine('Command not found: ' + command, 'error');
printLine('Type "help" for available commands.', '');
break;
}
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
processCommand(input.value);
input.value = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
document.addEventListener('click', function() {
input.focus();
});
loadProgress();
bootSequence();
Mission 1 is fully playable. The player can scan the network, discover hosts, connect to the target, and trigger mission completion. Next up, you will build Mission 2 where the player explores the file system and cracks a password hash.
Build Mission 2 (Password Cracking) and Progress System
Mission 1 is live. Players can scan the network, discover hosts, and connect to the target database server. The mission complete banner fires and Mission 2 unlocks.
Now you need to give players something to do once they're inside. Mission 2 challenges the player to explore the file system, find a password hash, and crack it using a simulated dictionary attack. You'll also wire up localStorage so progress persists between browser sessions.
In this step, get ready to:
- Implement the crack command with an animated dictionary attack.
- Add ls and cat commands for exploring the simulated file system.
- Wire up the command stubs and verify localStorage persistence.
Implement the crack command
A dictionary attack tries common passwords from a wordlist against a captured hash until one matches. Your crackPassword() function simulates this by cycling through six attempts with a delay between each, creating the illusion of a real cracking tool at work.
- In game.js, scroll down to just above the checkMission1() function.
- Add the crackPassword() function by pasting this code above checkMission1():
function crackPassword() {
if (!gameState.hashCaptured) {
printLine('No hash captured yet.', 'error');
printLine('Find a password hash file first.', '');
return;
}
if (gameState.passwordCracked) {
printLine('Password already cracked: netadmin2024', 'success');
return;
}
printLine('Starting dictionary attack...', 'system');
printLine('Loading wordlist: rockyou.txt (14,344,392 entries)', '');
printLine('', '');
// List of passwords to try, with the last one being the match
const attempts = [
'Trying: password123 ... FAIL',
'Trying: admin2023 ... FAIL',
'Trying: letmein ... FAIL',
'Trying: database1 ... FAIL',
'Trying: qwerty2024 ... FAIL',
'Trying: netadmin2024 ... MATCH!'
];
// Animate each attempt with a 400ms delay between them
let i = 0;
function tryNext() {
if (i < attempts.length) {
const isMatch = attempts[i].includes('MATCH');
printLine(' ' + attempts[i], isMatch ? 'success' : 'warning');
i++;
setTimeout(tryNext, 400);
} else {
printLine('', '');
printLine('[+] PASSWORD CRACKED!', 'success');
printLine(' Hash: $6$rounds=5000$saltsalt$kH3r...', '');
printLine(' Password: netadmin2024', 'success');
printLine('', '');
gameState.passwordCracked = true;
checkMission2();
}
}
tryNext();
}
What does this code do?
The function has three paths depending on the game state:
- If no hash has been captured yet, it shows an error telling the player to find one first.
- If the password is already cracked, it shows the result without re-running the animation.
- Otherwise, it loops through 6 password attempts with a 400ms delay using setTimeout recursion. The last attempt matches, triggering the success state and calling checkMission2().
You won't see a change yet. The checkMission2() function doesn't exist yet, so let's add it next.
- Directly below checkMission1(), add the checkMission2() function:
function checkMission2() {
if (gameState.passwordCracked && !gameState.missionComplete[2]) {
setTimeout(() => {
printLine('========================================', 'success');
printLine(' MISSION 2 COMPLETE: Password Cracking', 'success');
printLine('========================================', 'success');
printLine('', '');
printLine('All missions complete! You are a skilled operator.', 'system');
printLine('Type "status" to review your achievements.', '');
printLine('', '');
gameState.missionComplete[2] = true;
saveProgress();
}, 300);
}
}
What does this code do?
This mirrors checkMission1() but for the second mission. It checks whether the password has been cracked and the mission hasn't already been marked complete. If both are true, it prints a victory banner, sets missionComplete[2] to true, and calls saveProgress() to persist the win to localStorage.
- Save game.js.
Seeing a red error in the console?
If the browser console shows a reference error for checkMission2, make sure you placed the function after checkMission1() and before showStatus(). Check for missing closing braces on the function above it.
Help me debug my checkMission2 function
Add the ls and cat commands for file exploration
Before players can crack a hash, they need to find it. The ls and cat commands let players navigate the simulated file system on the target server, discovering directories and reading file contents until they uncover the password hash.
- In game.js, find the connectToHost() function. Directly below it, add the listFiles() function:
function listFiles() {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
printLine('Use "connect <ip>" to connect.', '');
return;
}
// Look up the current directory's contents from our fileSystem object
const fs = fileSystem['target-db'];
const contents = fs[currentDir];
if (!contents) {
printLine('Directory not found.', 'error');
return;
}
printLine('Contents of ' + currentDir + ':', 'info');
printLine('', '');
contents.forEach(item => {
// Check if the item is a directory by looking for it as a key in fileSystem
const isDir = fs['/' + item] || fs[currentDir + '/' + item];
const prefix = isDir ? 'drwxr-xr-x' : '-rw-r--r--';
const color = isDir ? 'info' : '';
printLine(' ' + prefix + ' ' + item, color);
});
printLine('', '');
printLine('Use "cat <filename>" to read files.', '');
printLine('Use "ls <dirname>" to enter a directory.', '');
}
What does this code do?
The function first checks that the player is connected to target-db. Then it looks up the current directory in the fileSystem object you defined earlier. Each item gets a Unix-style permission prefix (drwxr-xr-x for directories, -rw-r--r-- for files) to make the output look like a real Linux terminal.
You won't see a change yet. Next, add the helper that lets players navigate into subdirectories.
- Directly below listFiles(), add the listDir() function:
function listDir(dirname) {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
return;
}
const fs = fileSystem['target-db'];
let targetPath = dirname;
// Build the full path if the user typed a relative directory name
if (!dirname.startsWith('/')) {
targetPath = currentDir === '/' ? '/' + dirname : currentDir + '/' + dirname;
}
// If the path exists in our file system, move into it and show contents
if (fs[targetPath]) {
currentDir = targetPath;
listFiles();
} else {
printLine('Directory "' + dirname + '" not found.', 'error');
}
}
What does this code do?
When a player types ls backups, this function builds the full path from the current directory (e.g. /home/backups). If that path exists as a key in the fileSystem object, it updates currentDir and calls listFiles() to display the new directory's contents.
Now add the command that lets players read file contents. This is how they'll discover the password hash.
- Directly below listDir(), add the catFile() function:
function catFile(filename) {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
return;
}
if (!filename) {
printLine('Usage: cat <filename>', 'error');
return;
}
// Look up the file content from the files object
const fs = fileSystem['target-db'];
const content = fs.files[filename];
if (!content) {
printLine('File "' + filename + '" not found.', 'error');
return;
}
// Display the file content line by line
printLine('--- ' + filename + ' ---', 'info');
content.split('\n').forEach(line => printLine(' ' + line, ''));
printLine('---', 'info');
// Special trigger: reading shadow.bak captures the hash
if (filename === 'shadow.bak') {
gameState.hashCaptured = true;
printLine('', '');
printLine('[!] Password hash captured!', 'success');
printLine(' Use "crack" to attempt to crack it.', 'warning');
}
}
What does this code do?
The function reads file content from fileSystem['target-db'].files and prints it to the terminal. The key gameplay trigger is at the bottom: when the player reads shadow.bak, it sets gameState.hashCaptured = true. This unlocks the crack command.
- Save game.js.
Not sure where to place these functions?
The order should be: connectToHost(), then listFiles(), then listDir(), then catFile(), then crackPassword(). If you're unsure, use the full code tab at the end of this step to compare.
Help me check my function placement in game.js
Wire up the commands and verify localStorage persistence
Your functions exist but the command stubs in processCommand() are still empty. You need to connect each case to its function so player input actually triggers the right behavior.
- In game.js, scroll down to the processCommand() function.
- Find the three empty stub cases. They look like this:
case 'ls':
break;
case 'cat':
break;
case 'crack':
break;
- Replace those three cases with the following code:
case 'ls':
if (args) {
listDir(args);
} else {
listFiles();
}
break;
case 'cat':
catFile(args);
break;
case 'crack':
crackPassword();
break;
What does this code do?
The ls case checks if the player typed a directory name after it. If so, it calls listDir(args) to navigate into that directory. If not, it calls listFiles() to show the current directory.
The cat and crack cases simply pass through to their functions. The args variable (defined earlier in processCommand) captures everything the player typed after the command name.
How does localStorage persistence work here?
The saveProgress() and loadProgress() functions were set up in the previous step. Now that checkMission2() calls saveProgress(), the full persistence loop is active. Every time a mission completes, the entire gameState object is serialized to localStorage. When the player reopens the browser, loadProgress() restores their state.
- Save game.js and refresh your browser.
- Play through both missions: type scan, then connect 192.168.1.50, then ls, then ls backups, then cat shadow.bak, then crack.
- You should see the animated dictionary attack followed by a MISSION 2 COMPLETE banner.
- Close the browser tab, reopen index.html, and type status. Both missions should show COMPLETE, proving localStorage is working.
Commands not working after refresh?
If ls does nothing or crack shows no output, open the browser console (press F12 and click Console) to check for errors. Common causes:
- A typo in the case string (e.g. 'Ls' instead of 'ls').
- A missing break; after one of the cases, causing fall-through to the next case.
- The function was placed inside another function instead of at the top level.
Help me debug why my commands aren't working
✔️ Awesome, I've got everything!
Great. Double check you've saved game.js and that both missions play through to completion.
ⓧ I'd like to double check the full code
Here is the complete game.js file at this point:
const output = document.getElementById('terminal-output');
const input = document.getElementById('terminal-input');
const gameState = {
currentMission: 1,
missionComplete: { 1: false, 2: false },
discoveredHosts: [],
connectedHost: null,
hashCaptured: false,
passwordCracked: false
};
const networkHosts = [
{ ip: '192.168.1.1', name: 'gateway-router', ports: [22, 80, 443], os: 'RouterOS' },
{ ip: '192.168.1.10', name: 'workstation-01', ports: [22, 3389], os: 'Linux' },
{ ip: '192.168.1.25', name: 'file-server', ports: [21, 22, 445, 8080], os: 'Ubuntu 22.04' },
{ ip: '192.168.1.50', name: 'target-db', ports: [22, 3306, 5432], os: 'Debian 12' }
];
const fileSystem = {
'target-db': {
'/home': ['admin', 'backups'],
'/home/admin': ['.bash_history', 'notes.txt'],
'/home/backups': ['shadow.bak', 'readme.txt'],
files: {
'notes.txt': 'Reminder: update DB credentials. Current hash stored in /home/backups/shadow.bak',
'shadow.bak': 'admin:$6$rounds=5000$saltsalt$kH3rqWJzPG5x7VdYfOzKj2xR9mN4bT6wL1cA8pQ3dF==',
'readme.txt': 'Backup of shadow file. DO NOT share externally.',
'.bash_history': 'mysql -u admin -p\ncat /etc/shadow\ncp /etc/shadow /home/backups/shadow.bak'
}
}
};
let currentDir = '~';
let commandHistory = [];
let historyIndex = -1;
function printLine(text, className = '') {
const line = document.createElement('div');
line.className = 'output-line fade-in ' + className;
line.textContent = text;
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
function printLines(lines, className = '', delay = 80) {
let i = 0;
function next() {
if (i < lines.length) {
printLine(lines[i], className);
i++;
setTimeout(next, delay);
}
}
next();
}
function bootSequence() {
const bootLines = [
'[BOOT] Initializing NetHack Terminal v1.0...',
'[BOOT] Loading kernel modules... OK',
'[BOOT] Establishing encrypted connection... OK',
'[BOOT] Network interface: eth0 UP',
'[BOOT] IP assigned: 10.0.0.77',
'[BOOT] Firewall rules loaded.',
'[BOOT] System ready.',
'',
'================================================',
' NETHACK TERMINAL - Ethical Hacking Simulator',
' For educational purposes only.',
'================================================',
'',
'Welcome, Operator. You have 2 missions to complete.',
'Type "help" to see available commands.',
'Type "mission" to see your current objective.',
''
];
input.disabled = true;
let i = 0;
function nextLine() {
if (i < bootLines.length) {
const className = bootLines[i].startsWith('[BOOT]') ? 'system' : '';
printLine(bootLines[i], className);
i++;
const delay = bootLines[i - 1].startsWith('[BOOT]') ? 150 : 60;
setTimeout(nextLine, delay);
} else {
input.disabled = false;
input.focus();
}
}
nextLine();
}
function showHelp() {
const helpLines = [
'Available commands:',
'',
' help - Show this help message',
' mission - Show current mission objectives',
' scan - Scan the network for hosts',
' connect - Connect to a discovered host (usage: connect <ip>)',
' ls - List files in current directory',
' cat - Read a file (usage: cat <filename>)',
' crack - Attempt to crack a captured hash',
' clear - Clear the terminal',
' status - Show mission progress',
''
];
helpLines.forEach(line => printLine(line, 'info'));
}
function showMission() {
if (gameState.currentMission === 1 && !gameState.missionComplete[1]) {
printLine('MISSION 1: Network Reconnaissance', 'highlight');
printLine('', '');
printLine('Objective: Discover all hosts on the network and', '');
printLine('connect to the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Use "scan" to discover network hosts', gameState.discoveredHosts.length > 0 ? 'success' : '');
printLine(' 2. Identify the target database server', gameState.discoveredHosts.length >= 4 ? 'success' : '');
printLine(' 3. Use "connect <ip>" to access the target', gameState.connectedHost === 'target-db' ? 'success' : '');
} else if (gameState.currentMission === 2 && !gameState.missionComplete[2]) {
printLine('MISSION 2: Password Cracking', 'highlight');
printLine('', '');
printLine('Objective: Find and crack the admin password hash', '');
printLine('on the target database server.', '');
printLine('', '');
printLine('Steps:', 'info');
printLine(' 1. Explore the file system with "ls" and "cat"', gameState.hashCaptured ? 'success' : '');
printLine(' 2. Locate the password hash file', gameState.hashCaptured ? 'success' : '');
printLine(' 3. Use "crack" to run a dictionary attack', gameState.passwordCracked ? 'success' : '');
} else if (gameState.missionComplete[1] && gameState.missionComplete[2]) {
printLine('ALL MISSIONS COMPLETE!', 'success');
printLine('Congratulations, Operator. You have proven your skills.', 'system');
}
}
function scanNetwork() {
if (gameState.discoveredHosts.length >= 4) {
printLine('Network scan already complete. All hosts discovered.', 'warning');
return;
}
printLine('Initiating network scan on 192.168.1.0/24...', 'system');
printLine('', '');
let i = 0;
function revealHost() {
if (i < networkHosts.length) {
const host = networkHosts[i];
setTimeout(() => {
printLine(' [+] Host discovered: ' + host.ip, 'success');
printLine(' Name: ' + host.name, '');
printLine(' Ports: ' + host.ports.join(', '), '');
printLine(' OS: ' + host.os, '');
printLine('', '');
gameState.discoveredHosts.push(host);
if (i === networkHosts.length - 1) {
printLine('Scan complete. ' + networkHosts.length + ' hosts found.', 'system');
printLine('Hint: The target database server looks interesting...', 'warning');
checkMission1();
}
i++;
revealHost();
}, 600);
}
}
revealHost();
}
function connectToHost(ip) {
if (!ip) {
printLine('Usage: connect <ip>', 'error');
printLine('Example: connect 192.168.1.50', '');
return;
}
const host = gameState.discoveredHosts.find(h => h.ip === ip);
if (!host) {
printLine('Error: Host ' + ip + ' not found.', 'error');
printLine('Run "scan" first to discover hosts.', 'warning');
return;
}
printLine('Connecting to ' + ip + ' (' + host.name + ')...', 'system');
setTimeout(() => {
printLine('[+] Connection established via SSH (port 22)', 'success');
printLine(' Welcome to ' + host.name + ' (' + host.os + ')', '');
printLine('', '');
gameState.connectedHost = host.name;
currentDir = '/home';
checkMission1();
}, 500);
}
function listFiles() {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
printLine('Use "connect <ip>" to connect.', '');
return;
}
const fs = fileSystem['target-db'];
const contents = fs[currentDir];
if (!contents) {
printLine('Directory not found.', 'error');
return;
}
printLine('Contents of ' + currentDir + ':', 'info');
printLine('', '');
contents.forEach(item => {
const isDir = fs['/' + item] || fs[currentDir + '/' + item];
const prefix = isDir ? 'drwxr-xr-x' : '-rw-r--r--';
const color = isDir ? 'info' : '';
printLine(' ' + prefix + ' ' + item, color);
});
printLine('', '');
printLine('Use "cat <filename>" to read files.', '');
printLine('Use "ls <dirname>" to enter a directory.', '');
}
function listDir(dirname) {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
return;
}
const fs = fileSystem['target-db'];
let targetPath = dirname;
if (!dirname.startsWith('/')) {
targetPath = currentDir === '/' ? '/' + dirname : currentDir + '/' + dirname;
}
if (fs[targetPath]) {
currentDir = targetPath;
listFiles();
} else {
printLine('Directory "' + dirname + '" not found.', 'error');
}
}
function catFile(filename) {
if (gameState.connectedHost !== 'target-db') {
printLine('You must connect to a host first.', 'error');
return;
}
if (!filename) {
printLine('Usage: cat <filename>', 'error');
return;
}
const fs = fileSystem['target-db'];
const content = fs.files[filename];
if (!content) {
printLine('File "' + filename + '" not found.', 'error');
return;
}
printLine('--- ' + filename + ' ---', 'info');
content.split('\n').forEach(line => printLine(' ' + line, ''));
printLine('---', 'info');
if (filename === 'shadow.bak') {
gameState.hashCaptured = true;
printLine('', '');
printLine('[!] Password hash captured!', 'success');
printLine(' Use "crack" to attempt to crack it.', 'warning');
}
}
function crackPassword() {
if (!gameState.hashCaptured) {
printLine('No hash captured yet.', 'error');
printLine('Find a password hash file first.', '');
return;
}
if (gameState.passwordCracked) {
printLine('Password already cracked: netadmin2024', 'success');
return;
}
printLine('Starting dictionary attack...', 'system');
printLine('Loading wordlist: rockyou.txt (14,344,392 entries)', '');
printLine('', '');
const attempts = [
'Trying: password123 ... FAIL',
'Trying: admin2023 ... FAIL',
'Trying: letmein ... FAIL',
'Trying: database1 ... FAIL',
'Trying: qwerty2024 ... FAIL',
'Trying: netadmin2024 ... MATCH!'
];
let i = 0;
function tryNext() {
if (i < attempts.length) {
const isMatch = attempts[i].includes('MATCH');
printLine(' ' + attempts[i], isMatch ? 'success' : 'warning');
i++;
setTimeout(tryNext, 400);
} else {
printLine('', '');
printLine('[+] PASSWORD CRACKED!', 'success');
printLine(' Hash: $6$rounds=5000$saltsalt$kH3r...', '');
printLine(' Password: netadmin2024', 'success');
printLine('', '');
gameState.passwordCracked = true;
checkMission2();
}
}
tryNext();
}
function checkMission1() {
if (gameState.connectedHost === 'target-db' && gameState.discoveredHosts.length >= 4 && !gameState.missionComplete[1]) {
setTimeout(() => {
printLine('', '');
printLine('========================================', 'success');
printLine(' MISSION 1 COMPLETE: Reconnaissance', 'success');
printLine('========================================', 'success');
printLine('', '');
printLine('Mission 2 unlocked: Password Cracking', 'system');
printLine('Type "mission" to see your new objective.', '');
printLine('', '');
gameState.missionComplete[1] = true;
gameState.currentMission = 2;
saveProgress();
}, 300);
}
}
function checkMission2() {
if (gameState.passwordCracked && !gameState.missionComplete[2]) {
setTimeout(() => {
printLine('========================================', 'success');
printLine(' MISSION 2 COMPLETE: Password Cracking', 'success');
printLine('========================================', 'success');
printLine('', '');
printLine('All missions complete! You are a skilled operator.', 'system');
printLine('Type "status" to review your achievements.', '');
printLine('', '');
gameState.missionComplete[2] = true;
saveProgress();
}, 300);
}
}
function showStatus() {
printLine('--- OPERATOR STATUS ---', 'info');
printLine('', '');
printLine(' Mission 1 (Recon): ' + (gameState.missionComplete[1] ? 'COMPLETE' : 'IN PROGRESS'), gameState.missionComplete[1] ? 'success' : 'warning');
printLine(' Mission 2 (Cracking): ' + (gameState.missionComplete[2] ? 'COMPLETE' : gameState.currentMission >= 2 ? 'IN PROGRESS' : 'LOCKED'), gameState.missionComplete[2] ? 'success' : 'warning');
printLine('', '');
printLine(' Hosts discovered: ' + gameState.discoveredHosts.length + '/4', '');
printLine(' Connected to: ' + (gameState.connectedHost || 'none'), '');
printLine(' Hash captured: ' + (gameState.hashCaptured ? 'Yes' : 'No'), '');
printLine(' Password cracked: ' + (gameState.passwordCracked ? 'Yes' : 'No'), '');
printLine('', '');
}
function saveProgress() {
localStorage.setItem('nethack-save', JSON.stringify(gameState));
}
function loadProgress() {
const saved = localStorage.getItem('nethack-save');
if (saved) {
const data = JSON.parse(saved);
Object.assign(gameState, data);
}
}
function processCommand(cmd) {
const trimmed = cmd.trim();
if (!trimmed) return;
commandHistory.push(trimmed);
historyIndex = commandHistory.length;
printLine('user@nethack:~$ ' + trimmed, 'command');
const parts = trimmed.split(' ');
const command = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
switch (command) {
case 'help':
showHelp();
break;
case 'mission':
showMission();
break;
case 'scan':
scanNetwork();
break;
case 'connect':
connectToHost(args);
break;
case 'ls':
if (args) {
listDir(args);
} else {
listFiles();
}
break;
case 'cat':
catFile(args);
break;
case 'crack':
crackPassword();
break;
case 'clear':
output.innerHTML = '';
break;
case 'status':
showStatus();
break;
default:
printLine('Command not found: ' + command, 'error');
printLine('Type "help" for available commands.', '');
break;
}
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
processCommand(input.value);
input.value = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
document.addEventListener('click', function() {
input.focus();
});
loadProgress();
bootSequence();
Your game is fully playable with two complete missions and persistent progress. Players can explore, discover, and hack their way through both challenges. Next up, you'll take this further with a Secret Mission that adds a firewall bypass challenge.
Secret mission
Add Mission 3 (Firewall Bypass) with ASCII Network Map
Your 2-mission game is solid, but real penetration testers face layered defenses. In this secret mission, you'll add a third challenge where the player must bypass a firewall using a specific command sequence while racing against a trace detection system. You'll also build an ASCII art network map that updates in real time as nodes are compromised.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about.
Decide whether to keep your project files to continue playing or extending the game, or delete them entirely.
Resources you used:
- Project folder on your Desktop containing index.html, style.css, and game.js.
- Browser localStorage data (saved game progress under the nethack-save key).
Keep everything running
No action needed. Choose this if you want to keep playing the game or add new features.
- Your game files are just HTML, CSS, and JavaScript on your computer. They take up minimal disk space.
- Your saved progress stays in your browser's localStorage until you clear browsing data.
- You can reopen index.html in your browser anytime to play again.
Pause - I'll come back to this later
Since this project is just local files, there is nothing to pause. Your files and saved progress will be waiting for you whenever you come back.
- Close the browser tab running the game.
- Close VS Code if you are done for now.
- Your localStorage progress persists automatically. When you reopen index.html, your completed missions will still be saved.
Delete - I don't want to use this again
Remove all project files and saved data to start completely fresh.
- Clear the saved game data from your browser. Open your browser's developer console and run:
localStorage.removeItem('nethack-save')
- Delete your project folder. On macOS or Linux, open Terminal and run:
cd ~/Desktop
rm -rf nethack-terminal
- On Windows, open PowerShell and run:
cd ~\Desktop
Remove-Item -Recurse -Force nethack-terminal
- Confirm the folder is gone by running:
ls ~/Desktop
- You should no longer see nethack-terminal in the output.
Well Done!
Well Done!
Nice work! You just built a fully playable hacking simulation game that runs entirely in the browser using pure HTML, CSS, and JavaScript.
You've learned how to:
- Built a complete terminal-style UI with CRT scanlines, text glow, typing animations, and a blinking cursor using pure CSS keyframe animations.
- Created an interactive command parser in JavaScript that processes player input and generates contextual responses with DOM manipulation.
- Implemented 2 playable missions teaching real cybersecurity concepts (network reconnaissance and password cracking) with persistent progress saving via localStorage.
- Secret Mission: Extended the game with a firewall bypass mission featuring a trace-level mechanic and an ASCII network map visualization.
Ready to quiz yourself?