First version

This commit is contained in:
tcsenpai 2025-04-01 11:33:05 +02:00
commit 4d2e4f37af
24 changed files with 3110 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
build
*.lock

647
LEVELS.md Normal file
View File

@ -0,0 +1,647 @@
You're right! Let's enhance the LEVELS.md guide with more detailed examples of how to implement complex level mechanics like file system navigation, process management, and other Linux-like features.
````markdown:LEVELS.md
# LEVELS.md - Guide to Adding New Levels
## Introduction
Terminal Escape is designed to be easily expandable with new levels. This guide explains how to create and integrate new levels into the game.
## Level Structure
Each level is defined as an object that implements the `Level` interface. A level consists of:
1. **Basic Information**: ID, name, and description
2. **State Management**: Methods to initialize and manage level-specific state
3. **User Interface**: Methods to render the level and handle user input
4. **Hints**: An array of progressive hints for players who get stuck
## Creating a New Level
### Step 1: Create a new file
Create a new file in the `src/levels` directory, e.g., `levelX.ts` where X is the next level number.
### Step 2: Implement the Level interface
```typescript
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
import { levelUI } from '../ui/levelRenderer'; // Optional, for enhanced UI
const level: Level = {
id: X, // Replace X with the next level number
name: 'Your Level Name',
description: 'Brief description of your level',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
// Define your level-specific state here
// For example:
attempts: 0,
someFlag: false,
// Add any other state variables your level needs
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
// Display level information and UI
console.log('Your level description and UI goes here');
console.log('');
// Display available commands
console.log('Commands: "command1", "command2", etc.');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const command = input.trim();
// Split command into parts
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
// Handle different commands
if (cmd === 'command1') {
// Do something
return {
completed: false,
message: 'Response to command1'
};
}
// Handle level completion
if (cmd === 'win_command') {
return {
completed: true,
message: 'Congratulations! You completed the level.',
nextAction: 'next_level'
};
}
// Default response for unknown commands
return {
completed: false,
message: 'Unknown command. Try something else.'
};
},
hints: [
'First hint - very subtle',
'Second hint - more direct',
'Third hint - almost gives away the solution'
]
};
export function registerLevelX() { // Replace X with the level number
registerLevel(level);
}
````
### Step 3: Update the levels index file
Edit `src/levels/index.ts` to import and register your new level:
```typescript
import { registerLevel1 } from "./level1";
import { registerLevel2 } from "./level2";
// ... other levels
import { registerLevelX } from "./levelX"; // Add your new level
export function registerAllLevels() {
registerLevel1();
registerLevel2();
// ... other levels
registerLevelX(); // Register your new level
console.log("All levels registered successfully.");
}
```
## Implementing Complex Level Mechanics
### File System Navigation
To implement a file system level (like Level 2), you need to:
1. Create a virtual file system structure in your level state
2. Implement commands like `ls`, `cd`, and `cat`
3. Track the current directory and handle path navigation
Here's an example of a file system state structure:
```typescript
// In initialize()
gameState.levelStates[this.id] = {
currentDir: "/home/user",
fileSystem: {
"/home/user": {
type: "dir",
contents: ["Documents", "Pictures", ".hidden"],
},
"/home/user/Documents": {
type: "dir",
contents: ["notes.txt"],
},
"/home/user/Documents/notes.txt": {
type: "file",
content: "This is a text file.",
},
// Add more directories and files
},
};
```
Handling file system commands:
```typescript
// In handleInput()
if (cmd === "ls") {
// List directory contents
return {
completed: false,
message: fileSystem[levelState.currentDir].contents.join("\n"),
};
}
if (cmd === "cd" && parts.length > 1) {
const target = parts[1];
if (target === "..") {
// Go up one directory
const pathParts = levelState.currentDir.split("/");
if (pathParts.length > 2) {
pathParts.pop();
levelState.currentDir = pathParts.join("/");
return {
completed: false,
message: `Changed directory to ${levelState.currentDir}`,
};
}
} else {
// Go to specified directory
const newPath = `${levelState.currentDir}/${target}`;
if (fileSystem[newPath] && fileSystem[newPath].type === "dir") {
levelState.currentDir = newPath;
return {
completed: false,
message: `Changed directory to ${levelState.currentDir}`,
};
}
}
}
if (cmd === "cat" && parts.length > 1) {
const target = parts[1];
const path = `${levelState.currentDir}/${target}`;
if (fileSystem[path] && fileSystem[path].type === "file") {
return {
completed: false,
message: fileSystem[path].content,
};
}
}
```
### Process Management
For a process management level (like Level 3), you can:
1. Create a list of processes with properties like PID, name, CPU usage, etc.
2. Implement commands like `ps`, `kill`, and `start`
3. Track process states and check for completion conditions
Example process state:
```typescript
// In initialize()
gameState.levelStates[this.id] = {
processes: [
{ pid: 1, name: "systemd", cpu: 0.1, memory: 4.2, status: "running" },
{ pid: 423, name: "sshd", cpu: 0.0, memory: 1.1, status: "running" },
{
pid: 842,
name: "malware.bin",
cpu: 99.7,
memory: 85.5,
status: "running",
},
{ pid: 1024, name: "firewall", cpu: 0.1, memory: 1.8, status: "stopped" },
],
firewallStarted: false,
malwareKilled: false,
};
```
Handling process commands:
```typescript
// In handleInput()
if (cmd === "ps") {
// Format and display process list
let output = "PID NAME CPU% MEM% STATUS\n";
output += "--------------------------------------------\n";
levelState.processes.forEach((proc) => {
output += `${proc.pid.toString().padEnd(7)}${proc.name.padEnd(13)}${proc.cpu
.toFixed(1)
.padEnd(8)}${proc.memory.toFixed(1).padEnd(8)}${proc.status}\n`;
});
return {
completed: false,
message: output,
};
}
if (cmd === "kill" && parts.length > 1) {
const pid = parseInt(parts[1]);
const process = levelState.processes.find((p) => p.pid === pid);
if (process) {
process.status = "stopped";
if (process.name === "malware.bin") {
levelState.malwareKilled = true;
// Check if level is completed
if (levelState.firewallStarted) {
return {
completed: true,
message: "System secured! Malware stopped and firewall running.",
nextAction: "next_level",
};
}
}
}
}
if (cmd === "start" && parts.length > 1) {
const pid = parseInt(parts[1]);
const process = levelState.processes.find((p) => p.pid === pid);
if (process) {
process.status = "running";
if (process.name === "firewall") {
levelState.firewallStarted = true;
// Check if level is completed
if (levelState.malwareKilled) {
return {
completed: true,
message: "System secured! Malware stopped and firewall running.",
nextAction: "next_level",
};
}
}
}
}
```
### File Permissions
For a permissions-based level (like Level 4):
1. Create files with permission attributes
2. Implement commands like `chmod` and `sudo`
3. Check permissions before allowing file access
Example permissions state:
```typescript
// In initialize()
gameState.levelStates[this.id] = {
files: [
{
name: "README.txt",
permissions: "rw-r--r--",
owner: "user",
group: "user",
},
{
name: "secret_data.db",
permissions: "----------",
owner: "root",
group: "root",
},
{
name: "change_permissions.sh",
permissions: "r--------",
owner: "user",
group: "user",
},
],
currentUser: "user",
sudoAvailable: false,
};
```
Handling permission commands:
```typescript
// In handleInput()
if (cmd === "cat" && parts.length > 1) {
const fileName = parts[1];
const file = levelState.files.find((f) => f.name === fileName);
if (file) {
// Check if user has read permission
const canRead =
(levelState.currentUser === file.owner && file.permissions[0] === "r") ||
(levelState.currentUser !== file.owner &&
levelState.currentUser === file.group &&
file.permissions[3] === "r") ||
(levelState.currentUser !== file.owner &&
levelState.currentUser !== file.group &&
file.permissions[6] === "r") ||
levelState.currentUser === "root"; // root can read anything
if (!canRead) {
return {
completed: false,
message: `Permission denied: Cannot read ${fileName}`,
};
}
// Return file content based on the file name
if (fileName === "README.txt") {
return {
completed: false,
message:
"This system contains important data. You need to access secret_data.db to proceed.",
};
}
// Handle other files...
}
}
if (cmd === "chmod" && parts.length > 2) {
const permissions = parts[1]; // e.g., +x
const fileName = parts[2];
const file = levelState.files.find((f) => f.name === fileName);
if (file) {
// Check if user owns the file
if (
levelState.currentUser !== file.owner &&
levelState.currentUser !== "root"
) {
return {
completed: false,
message: `Permission denied: Only the owner can change permissions of ${fileName}`,
};
}
// Handle different chmod formats
if (permissions === "+x") {
// Make file executable
let newPermissions = file.permissions.split("");
newPermissions[2] = "x"; // Owner execute
file.permissions = newPermissions.join("");
if (fileName === "change_permissions.sh") {
levelState.scriptExecutable = true;
}
return {
completed: false,
message: `Changed permissions of ${fileName} to ${file.permissions}`,
};
}
// Handle other permission changes...
}
}
```
### Network Configuration
For a network-based level (like Level 5):
1. Create network interfaces, firewall rules, and DNS settings
2. Implement commands like `ifconfig`, `ping`, and `firewall-cmd`
3. Track network state and check for connectivity
Example network state:
```typescript
// In initialize()
gameState.levelStates[this.id] = {
interfaces: [
{ name: "lo", status: "UP", ip: "127.0.0.1", netmask: "255.0.0.0" },
{ name: "eth0", status: "DOWN", ip: "", netmask: "" },
],
firewall: {
enabled: true,
rules: [
{ port: 80, protocol: "tcp", action: "DENY" },
{ port: 443, protocol: "tcp", action: "DENY" },
],
},
dns: {
configured: false,
server: "",
},
gateway: {
configured: false,
address: "",
},
};
```
Handling network commands:
```typescript
// In handleInput()
if (cmd === "ifconfig") {
if (parts.length === 1) {
// Show all interfaces
let output = "Network Interfaces:\n";
output += "NAME STATUS IP NETMASK\n";
output += "----------------------------------------\n";
levelState.interfaces.forEach((iface) => {
output += `${iface.name.padEnd(7)}${iface.status.padEnd(
9
)}${iface.ip.padEnd(14)}${iface.netmask}\n`;
});
return {
completed: false,
message: output,
};
} else if (parts.length >= 4) {
// Configure an interface
const ifaceName = parts[1];
const ip = parts[2];
const netmask = parts[3];
const iface = levelState.interfaces.find((i) => i.name === ifaceName);
if (iface) {
iface.ip = ip;
iface.netmask = netmask;
return {
completed: false,
message: `Configured ${ifaceName} with IP ${ip} and netmask ${netmask}.`,
};
}
}
}
if (cmd === "ifup" && parts.length > 1) {
const ifaceName = parts[1];
const iface = levelState.interfaces.find((i) => i.name === ifaceName);
if (iface) {
iface.status = "UP";
return {
completed: false,
message: `Interface ${ifaceName} is now UP.`,
};
}
}
if (cmd === "firewall-cmd") {
if (parts.includes("--disable")) {
levelState.firewall.enabled = false;
return {
completed: false,
message: "Firewall disabled.",
};
}
if (parts.includes("--allow") && parts.length > 2) {
const port = parseInt(parts[parts.indexOf("--allow") + 1]);
// Find the rule for this port
const rule = levelState.firewall.rules.find((r) => r.port === port);
if (rule) {
rule.action = "ALLOW";
return {
completed: false,
message: `Port ${port} is now allowed through the firewall.`,
};
}
}
}
```
## Using the Enhanced UI
The game includes a `levelUI` helper in `src/ui/levelRenderer.ts` that provides styled UI components for your levels:
```typescript
import { levelUI } from "../ui/levelRenderer";
// In render()
levelUI.title("Welcome to My Level");
levelUI.paragraph("This is a description of the level.");
levelUI.spacer();
// Display a terminal
levelUI.terminal(
"$ ls -la\ntotal 12\ndrwxr-xr-x 2 user user 4096 Jan 1 12:00 ."
);
// Display a file system
const items = [
{ name: "Documents", type: "dir" },
{ name: "notes.txt", type: "file" },
];
levelUI.fileSystem("/home/user", items);
// Display a process table
levelUI.processTable(levelState.processes);
// Display available commands
levelUI.commands(["ls", "cd [dir]", "cat [file]"]);
```
## Level State Management
The level state is stored in `gameState.levelStates[levelId]`. This is where you should store any level-specific data that needs to persist between renders or commands.
## Level Results
When handling user input, your level should return a `LevelResult` object:
```typescript
interface LevelResult {
completed: boolean; // Whether the level is completed
message?: string; // Optional message to display to the user
nextAction?: "next_level" | "main_menu" | "continue"; // What to do next
}
```
- Set `completed: true` when the player solves the level
- Use `nextAction: 'next_level'` to proceed to the next level
- Use `nextAction: 'main_menu'` to return to the main menu
- Use `nextAction: 'continue'` or omit to stay in the current level
## Best Practices
1. **Initialization**: Always check if the level state exists and initialize it if not
2. **Progressive Difficulty**: Make your level challenging but fair
3. **Clear Instructions**: Make sure players understand what they need to do
4. **Meaningful Feedback**: Provide helpful responses to player commands
5. **Multiple Solutions**: When possible, allow multiple ways to solve the puzzle
6. **Thematic Consistency**: Try to maintain the Linux/tech theme of the game
7. **Hints**: Provide at least 3 hints of increasing specificity
## Example Level Ideas
- **Network Security**: Configure a firewall to block specific attacks
- **Cryptography**: Decode encrypted messages using various ciphers
- **Database Challenge**: Use SQL-like commands to extract information
- **Git Simulation**: Navigate and manipulate a git repository
- **Container Escape**: Escape from a simulated container environment
## Testing Your Level
Always test your level thoroughly to ensure:
- It can be completed
- All commands work as expected
- The level state initializes correctly
- Transitions to the next level work properly
Happy level creating!
```
This enhanced guide provides much more detailed examples of how to implement complex level mechanics like file system navigation, process management, permissions, and network configuration. It should help anyone who wants to create new levels with sophisticated Linux-like features.
```

3
leaderboard.json Normal file
View File

@ -0,0 +1,3 @@
{
"players": []
}

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "terminal-escape",
"version": "1.0.0",
"description": "A Linux terminal escape room game",
"main": "src/index.ts",
"type": "module",
"scripts": {
"start": "node -e \"console.log('\\x1b[2J\\x1b[0f\\x1b[36m\\n Launching Terminal Escape...\\n\\x1b[0m'); setTimeout(() => {}, 1000)\" && bun src/index.ts",
"build": "bun build src/index.ts --outdir ./dist",
"dev": "bun --watch src/index.ts"
},
"author": "",
"license": "MIT",
"devDependencies": {
"bun-types": "latest"
},
"dependencies": {
"kleur": "^4.1.5"
}
}

217
saves/cris.json Normal file
View File

@ -0,0 +1,217 @@
{
"playerName": "cris",
"currentLevel": 5,
"startTime": 1743499540345,
"lastSaveTime": 1743499661851,
"completedLevels": [
1,
2,
3,
4
],
"inventory": [],
"levelStates": {
"1": {
"attempts": 1,
"foundClue1": false,
"foundClue2": false
},
"2": {
"currentDir": "/home/user/.hidden",
"foundKey": true,
"fileSystem": {
"/home/user": {
"type": "dir",
"contents": [
"Documents",
"Pictures",
".hidden"
]
},
"/home/user/Documents": {
"type": "dir",
"contents": [
"notes.txt",
"secret"
]
},
"/home/user/Documents/notes.txt": {
"type": "file",
"content": "Remember to check hidden files. They start with a dot."
},
"/home/user/Documents/secret": {
"type": "dir",
"contents": [
"decoy.key"
]
},
"/home/user/Documents/secret/decoy.key": {
"type": "file",
"content": "Nice try, but this is not the real key!"
},
"/home/user/Pictures": {
"type": "dir",
"contents": [
"vacation.jpg"
]
},
"/home/user/Pictures/vacation.jpg": {
"type": "file",
"content": "Just a picture of a beach. Nothing special here."
},
"/home/user/.hidden": {
"type": "dir",
"contents": [
"system.key"
]
},
"/home/user/.hidden/system.key": {
"type": "file",
"content": "REAL_KEY_FOUND"
}
}
},
"3": {
"processes": [
{
"pid": 1,
"name": "systemd",
"cpu": 0.1,
"memory": 4.2,
"status": "running"
},
{
"pid": 423,
"name": "sshd",
"cpu": 0,
"memory": 1.1,
"status": "running"
},
{
"pid": 587,
"name": "nginx",
"cpu": 0.2,
"memory": 2.3,
"status": "running"
},
{
"pid": 842,
"name": "malware.bin",
"cpu": 99.7,
"memory": 85.5,
"status": "stopped"
},
{
"pid": 967,
"name": "bash",
"cpu": 0,
"memory": 0.5,
"status": "running"
},
{
"pid": 1024,
"name": "firewall",
"cpu": 0.1,
"memory": 1.8,
"status": "running"
}
],
"firewallStarted": true,
"malwareKilled": true
},
"4": {
"files": [
{
"name": "README.txt",
"permissions": "rw-r--r--",
"owner": "user",
"group": "user"
},
{
"name": "secret_data.db",
"permissions": "----------",
"owner": "root",
"group": "root"
},
{
"name": "change_permissions.sh",
"permissions": "r-x------",
"owner": "user",
"group": "user"
},
{
"name": "access_key.bin",
"permissions": "rw-------",
"owner": "root",
"group": "user"
}
],
"currentUser": "user",
"sudoAvailable": true,
"scriptExecutable": true,
"accessKeyReadable": false
},
"5": {
"interfaces": [
{
"name": "lo",
"status": "UP",
"ip": "127.0.0.1",
"netmask": "255.0.0.0"
},
{
"name": "eth0",
"status": "DOWN",
"ip": "",
"netmask": ""
},
{
"name": "wlan0",
"status": "DOWN",
"ip": "",
"netmask": ""
}
],
"firewall": {
"enabled": true,
"rules": [
{
"port": 22,
"protocol": "tcp",
"action": "DENY"
},
{
"port": 80,
"protocol": "tcp",
"action": "DENY"
},
{
"port": 443,
"protocol": "tcp",
"action": "DENY"
},
{
"port": 8080,
"protocol": "tcp",
"action": "DENY"
}
]
},
"dns": {
"configured": false,
"server": ""
},
"gateway": {
"configured": false,
"address": ""
},
"connections": [],
"escapePortal": {
"host": "escape.portal",
"ip": "10.0.0.1",
"port": 8080
},
"hintIndex": 1
}
}
}

15
saves/cris_autosave.json Normal file
View File

@ -0,0 +1,15 @@
{
"playerName": "cris",
"currentLevel": 1,
"startTime": 1743499840422,
"lastSaveTime": 1743499840423,
"completedLevels": [],
"inventory": [],
"levelStates": {
"1": {
"attempts": 0,
"foundClue1": false,
"foundClue2": false
}
}
}

View File

@ -0,0 +1,73 @@
{
"playerName": "senpai",
"currentLevel": 2,
"startTime": 1743499361975,
"lastSaveTime": 1743499410663,
"completedLevels": [
1,
2
],
"inventory": [],
"levelStates": {
"1": {
"attempts": 1,
"foundClue1": false,
"foundClue2": false
},
"2": {
"currentDir": "/home/user/.hidden",
"foundKey": true,
"fileSystem": {
"/home/user": {
"type": "dir",
"contents": [
"Documents",
"Pictures",
".hidden"
]
},
"/home/user/Documents": {
"type": "dir",
"contents": [
"notes.txt",
"secret"
]
},
"/home/user/Documents/notes.txt": {
"type": "file",
"content": "Remember to check hidden files. They start with a dot."
},
"/home/user/Documents/secret": {
"type": "dir",
"contents": [
"decoy.key"
]
},
"/home/user/Documents/secret/decoy.key": {
"type": "file",
"content": "Nice try, but this is not the real key!"
},
"/home/user/Pictures": {
"type": "dir",
"contents": [
"vacation.jpg"
]
},
"/home/user/Pictures/vacation.jpg": {
"type": "file",
"content": "Just a picture of a beach. Nothing special here."
},
"/home/user/.hidden": {
"type": "dir",
"contents": [
"system.key"
]
},
"/home/user/.hidden/system.key": {
"type": "file",
"content": "REAL_KEY_FOUND"
}
}
}
}
}

47
src/core/gameInit.ts Normal file
View File

@ -0,0 +1,47 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// Get the directory name of the current module
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Game directories
const SAVE_DIR = path.join(__dirname, '../../saves');
const LEADERBOARD_PATH = path.join(__dirname, '../../leaderboard.json');
export async function initializeGame() {
// Ensure save directory exists
try {
await fs.mkdir(SAVE_DIR, { recursive: true });
console.log('Save directory initialized.');
} catch (error) {
console.error('Failed to create save directory:', error);
}
// Ensure leaderboard file exists
try {
try {
await fs.access(LEADERBOARD_PATH);
} catch {
// Create empty leaderboard if it doesn't exist
await fs.writeFile(LEADERBOARD_PATH, JSON.stringify({ players: [] }, null, 2));
}
console.log('Leaderboard initialized.');
} catch (error) {
console.error('Failed to initialize leaderboard:', error);
}
}
// Helper functions for save management
export async function listSaves() {
try {
const files = await fs.readdir(SAVE_DIR);
return files.filter(file => file.endsWith('.json'));
} catch (error) {
console.error('Failed to list saves:', error);
return [];
}
}
export const getSavePath = (saveName: string) => path.join(SAVE_DIR, `${saveName}.json`);
export const getLeaderboardPath = () => LEADERBOARD_PATH;

75
src/core/gameState.ts Normal file
View File

@ -0,0 +1,75 @@
import fs from 'fs/promises';
import { getSavePath } from './gameInit';
export interface GameState {
playerName: string;
currentLevel: number;
startTime: number;
lastSaveTime: number;
completedLevels: number[];
inventory: string[];
levelStates: Record<number, any>;
}
let currentGameState: GameState | null = null;
export function getCurrentGameState(): GameState | null {
return currentGameState;
}
export function createNewGame(playerName: string): GameState {
const newState: GameState = {
playerName,
currentLevel: 1,
startTime: Date.now(),
lastSaveTime: Date.now(),
completedLevels: [],
inventory: [],
levelStates: {}
};
currentGameState = newState;
return newState;
}
export async function saveGame(saveName?: string): Promise<boolean> {
if (!currentGameState) {
console.error('No active game to save');
return false;
}
// Update save time
currentGameState.lastSaveTime = Date.now();
// Use player name as save name if not specified
const fileName = saveName || currentGameState.playerName;
try {
await fs.writeFile(
getSavePath(fileName),
JSON.stringify(currentGameState, null, 2)
);
console.log(`Game saved as ${fileName}`);
return true;
} catch (error) {
console.error('Failed to save game:', error);
return false;
}
}
export async function loadGame(saveName: string): Promise<boolean> {
try {
const saveData = await fs.readFile(getSavePath(saveName), 'utf-8');
currentGameState = JSON.parse(saveData) as GameState;
console.log(`Game loaded: ${saveName}`);
return true;
} catch (error) {
console.error('Failed to load game:', error);
return false;
}
}
export async function autoSave(): Promise<boolean> {
if (!currentGameState) return false;
return saveGame(`${currentGameState.playerName}_autosave`);
}

63
src/core/leaderboard.ts Normal file
View File

@ -0,0 +1,63 @@
import fs from 'fs/promises';
import { getLeaderboardPath } from './gameInit';
interface LeaderboardEntry {
playerName: string;
completionTime: number; // in milliseconds
completionDate: string;
}
interface Leaderboard {
players: LeaderboardEntry[];
}
export async function getLeaderboard(): Promise<Leaderboard> {
try {
const data = await fs.readFile(getLeaderboardPath(), 'utf-8');
return JSON.parse(data) as Leaderboard;
} catch (error) {
console.error('Failed to read leaderboard:', error);
return { players: [] };
}
}
export async function updateLeaderboard(
playerName: string,
completionTime: number
): Promise<boolean> {
try {
const leaderboard = await getLeaderboard();
// Add new entry
leaderboard.players.push({
playerName,
completionTime,
completionDate: new Date().toISOString()
});
// Sort by completion time (fastest first)
leaderboard.players.sort((a, b) => a.completionTime - b.completionTime);
// Save updated leaderboard
await fs.writeFile(
getLeaderboardPath(),
JSON.stringify(leaderboard, null, 2)
);
return true;
} catch (error) {
console.error('Failed to update leaderboard:', error);
return false;
}
}
export function formatTime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
const remainingSeconds = seconds % 60;
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
}

77
src/core/levelSystem.ts Normal file
View File

@ -0,0 +1,77 @@
import { getCurrentGameState, autoSave } from './gameState';
import { updateLeaderboard } from './leaderboard';
export interface Level {
id: number;
name: string;
description: string;
initialize: () => Promise<void>;
render: () => Promise<void>;
handleInput: (input: string) => Promise<LevelResult>;
hints: string[];
}
export interface LevelResult {
completed: boolean;
message?: string;
nextAction?: 'next_level' | 'main_menu' | 'continue';
}
// Registry of all available levels
const levels: Record<number, Level> = {};
export function registerLevel(level: Level) {
levels[level.id] = level;
console.log(`Registered level: ${level.id} - ${level.name}`);
}
export function getLevelById(id: number): Level | undefined {
return levels[id];
}
export function getAllLevels(): Level[] {
return Object.values(levels).sort((a, b) => a.id - b.id);
}
export async function startLevel(levelId: number): Promise<boolean> {
const gameState = getCurrentGameState();
if (!gameState) {
console.error('No active game');
return false;
}
const level = getLevelById(levelId);
if (!level) {
console.error(`Level ${levelId} not found`);
return false;
}
gameState.currentLevel = levelId;
// Initialize level
await level.initialize();
// Auto-save when starting a new level
await autoSave();
return true;
}
export async function completeCurrentLevel(): Promise<void> {
const gameState = getCurrentGameState();
if (!gameState) return;
if (!gameState.completedLevels.includes(gameState.currentLevel)) {
gameState.completedLevels.push(gameState.currentLevel);
}
// If this was the final level, update the leaderboard
const allLevels = getAllLevels();
if (gameState.completedLevels.length === allLevels.length) {
const totalTime = Date.now() - gameState.startTime;
await updateLeaderboard(gameState.playerName, totalTime);
}
// Auto-save on level completion
await autoSave();
}

21
src/index.ts Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bun
import { renderMainMenu } from './ui/mainMenu';
import { initializeGame } from './core/gameInit';
import { registerAllLevels } from './levels';
async function main() {
// Initialize game systems
await initializeGame();
// Register all game levels
registerAllLevels();
// Render the main menu to start
await renderMainMenu();
}
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});

15
src/levels/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { registerLevel1 } from './level1';
import { registerLevel2 } from './level2';
import { registerLevel3 } from './level3';
import { registerLevel4 } from './level4';
import { registerLevel5 } from './level5';
export function registerAllLevels() {
registerLevel1();
registerLevel2();
registerLevel3();
registerLevel4();
registerLevel5();
console.log('All levels registered successfully.');
}

119
src/levels/level1.ts Normal file
View File

@ -0,0 +1,119 @@
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
const level: Level = {
id: 1,
name: 'The Locked Terminal',
description: 'You find yourself in front of a locked terminal. You need to find the password to proceed.',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
attempts: 0,
foundClue1: false,
foundClue2: false
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
const levelState = gameState.levelStates[this.id];
console.log('You find yourself in a dimly lit room with a computer terminal.');
console.log('The screen shows a password prompt, and you need to get in.');
console.log('');
console.log('The terminal reads:');
console.log('');
console.log(' ╔════════════════════════════════════╗');
console.log(' ║ SYSTEM LOCKED ║');
console.log(' ║ ║');
console.log(' ║ Enter password: ║');
console.log(' ║ Hint: The admin loves penguins ║');
console.log(' ╚════════════════════════════════════╝');
console.log('');
if (levelState.foundClue1) {
console.log('You found a sticky note that says: "The password is the mascot\'s name"');
}
if (levelState.foundClue2) {
console.log('You found a book about Linux with a page bookmarked about Tux.');
}
console.log('');
console.log('Commands: "look around", "check desk", "check drawer", "enter [password]"');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
const levelState = gameState.levelStates[this.id];
const command = input.toLowerCase().trim();
if (command === 'look around') {
return {
completed: false,
message: 'You see a desk with a computer on it. There\'s a drawer in the desk and some books on a shelf.'
};
}
if (command === 'check desk') {
levelState.foundClue1 = true;
return {
completed: false,
message: 'You found a sticky note that says: "The password is the mascot\'s name"'
};
}
if (command === 'check drawer') {
levelState.foundClue2 = true;
return {
completed: false,
message: 'You found a book about Linux with a page bookmarked about Tux.'
};
}
if (command.startsWith('enter ')) {
const password = command.substring(6).trim().toLowerCase();
levelState.attempts++;
if (password === 'tux') {
return {
completed: true,
message: 'Access granted! The terminal unlocks, revealing the next challenge.',
nextAction: 'next_level'
};
} else {
return {
completed: false,
message: `Incorrect password. The system rejects your attempt. (Attempt ${levelState.attempts})`
};
}
}
return {
completed: false,
message: 'Unknown command. Try something else.'
};
},
hints: [
'Try looking around the room for clues.',
'The password is related to Linux.',
'Tux is the Linux mascot - a penguin.'
]
};
export function registerLevel1() {
registerLevel(level);
}

248
src/levels/level2.ts Normal file
View File

@ -0,0 +1,248 @@
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
const level: Level = {
id: 2,
name: 'File System Maze',
description: 'Navigate through a virtual file system to find the key.',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
currentDir: '/home/user',
foundKey: false,
fileSystem: {
'/home/user': {
type: 'dir',
contents: ['Documents', 'Pictures', '.hidden']
},
'/home/user/Documents': {
type: 'dir',
contents: ['notes.txt', 'secret']
},
'/home/user/Documents/notes.txt': {
type: 'file',
content: 'Remember to check hidden files. They start with a dot.'
},
'/home/user/Documents/secret': {
type: 'dir',
contents: ['decoy.key']
},
'/home/user/Documents/secret/decoy.key': {
type: 'file',
content: 'Nice try, but this is not the real key!'
},
'/home/user/Pictures': {
type: 'dir',
contents: ['vacation.jpg']
},
'/home/user/Pictures/vacation.jpg': {
type: 'file',
content: 'Just a picture of a beach. Nothing special here.'
},
'/home/user/.hidden': {
type: 'dir',
contents: ['system.key']
},
'/home/user/.hidden/system.key': {
type: 'file',
content: 'REAL_KEY_FOUND'
}
}
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const currentDir = levelState.currentDir;
const fileSystem = levelState.fileSystem;
console.log('You\'re in a virtual file system and need to find the system key.');
console.log('');
console.log(`Current directory: ${currentDir}`);
console.log('');
if (fileSystem[currentDir].type === 'dir') {
console.log('Contents:');
if (fileSystem[currentDir].contents.length === 0) {
console.log(' (empty directory)');
} else {
fileSystem[currentDir].contents.forEach(item => {
const path = `${currentDir}/${item}`;
const type = fileSystem[path].type === 'dir' ? 'Directory' : 'File';
console.log(` ${item} (${type})`);
});
}
} else {
console.log('File content:');
console.log(fileSystem[currentDir].content);
}
console.log('');
console.log('Commands: "ls", "cd [dir]", "cat [file]", "pwd", "find [name]"');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const fileSystem = levelState.fileSystem;
const command = input.trim();
// Split command into parts
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
if (cmd === 'ls') {
// List directory contents
return {
completed: false,
message: fileSystem[levelState.currentDir].contents.join('\n')
};
}
if (cmd === 'pwd') {
// Print working directory
return {
completed: false,
message: levelState.currentDir
};
}
if (cmd === 'cd' && parts.length > 1) {
// Change directory
const target = parts[1];
if (target === '..') {
// Go up one directory
const pathParts = levelState.currentDir.split('/');
if (pathParts.length > 2) { // Don't go above /home/user
pathParts.pop();
levelState.currentDir = pathParts.join('/');
return {
completed: false,
message: `Changed directory to ${levelState.currentDir}`
};
} else {
return {
completed: false,
message: 'Cannot go above the home directory.'
};
}
} else if (target === '.') {
// Stay in current directory
return {
completed: false,
message: `Still in ${levelState.currentDir}`
};
} else {
// Go to specified directory
const newPath = `${levelState.currentDir}/${target}`;
if (fileSystem[newPath] && fileSystem[newPath].type === 'dir') {
levelState.currentDir = newPath;
return {
completed: false,
message: `Changed directory to ${levelState.currentDir}`
};
} else {
return {
completed: false,
message: `Cannot change to ${target}: No such directory`
};
}
}
}
if (cmd === 'cat' && parts.length > 1) {
// View file contents
const target = parts[1];
const filePath = `${levelState.currentDir}/${target}`;
if (fileSystem[filePath] && fileSystem[filePath].type === 'file') {
const content = fileSystem[filePath].content;
// Check if this is the key file
if (filePath === '/home/user/.hidden/system.key') {
levelState.foundKey = true;
return {
completed: true,
message: `You found the system key! The file contains: ${content}`,
nextAction: 'next_level'
};
}
return {
completed: false,
message: `File contents: ${content}`
};
} else {
return {
completed: false,
message: `Cannot read ${target}: No such file`
};
}
}
if (cmd === 'find' && parts.length > 1) {
// Simple find implementation
const target = parts[1];
const results: string[] = [];
// Search through the file system
Object.keys(fileSystem).forEach(path => {
if (path.includes(target)) {
results.push(path);
}
});
if (results.length > 0) {
return {
completed: false,
message: `Found matches:\n${results.join('\n')}`
};
} else {
return {
completed: false,
message: `No matches found for "${target}"`
};
}
}
return {
completed: false,
message: 'Unknown command or invalid syntax.'
};
},
hints: [
'Try using basic Linux commands like "ls", "cd", and "cat".',
'Remember that hidden files and directories start with a dot (.)',
'Use "ls" to list files, "cd" to change directories, and "cat" to view file contents.'
]
};
export function registerLevel2() {
registerLevel(level);
}

227
src/levels/level3.ts Normal file
View File

@ -0,0 +1,227 @@
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
const level: Level = {
id: 3,
name: 'Process Control',
description: 'Manage system processes to unlock the next level.',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
processes: [
{ pid: 1, name: 'systemd', cpu: 0.1, memory: 4.2, status: 'running' },
{ pid: 423, name: 'sshd', cpu: 0.0, memory: 1.1, status: 'running' },
{ pid: 587, name: 'nginx', cpu: 0.2, memory: 2.3, status: 'running' },
{ pid: 842, name: 'malware.bin', cpu: 99.7, memory: 85.5, status: 'running' },
{ pid: 967, name: 'bash', cpu: 0.0, memory: 0.5, status: 'running' },
{ pid: 1024, name: 'firewall', cpu: 0.1, memory: 1.8, status: 'stopped' }
],
firewallStarted: false,
malwareKilled: false
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
console.log('You\'ve gained access to the system\'s process manager.');
console.log('Something seems to be consuming a lot of resources.');
console.log('You need to stop the malicious process and start the firewall.');
console.log('');
console.log('Current processes:');
console.log('PID NAME CPU% MEM% STATUS');
console.log('--------------------------------------------');
levelState.processes.forEach(proc => {
console.log(
`${proc.pid.toString().padEnd(7)}${proc.name.padEnd(13)}${proc.cpu.toFixed(1).padEnd(8)}${proc.memory.toFixed(1).padEnd(8)}${proc.status}`
);
});
console.log('');
console.log('System status: ' + (levelState.malwareKilled && levelState.firewallStarted ?
'SECURE' : 'VULNERABLE'));
console.log('');
console.log('Commands: "ps", "kill [pid]", "start [pid]", "info [pid]"');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const command = input.trim();
// Split command into parts
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
if (cmd === 'ps') {
// Just show processes again (same as render)
return {
completed: false,
message: 'Process list displayed.'
};
}
if (cmd === 'kill' && parts.length > 1) {
const pid = parseInt(parts[1]);
const process = levelState.processes.find(p => p.pid === pid);
if (!process) {
return {
completed: false,
message: `No process with PID ${pid} found.`
};
}
if (process.status === 'stopped') {
return {
completed: false,
message: `Process ${pid} (${process.name}) is already stopped.`
};
}
// Stop the process
process.status = 'stopped';
// Check if it was the malware
if (process.name === 'malware.bin') {
levelState.malwareKilled = true;
// Check if level is completed
if (levelState.firewallStarted) {
return {
completed: true,
message: 'System secured! Malware stopped and firewall running.',
nextAction: 'next_level'
};
}
return {
completed: false,
message: `Killed malicious process ${pid} (${process.name}). Now start the firewall!`
};
}
return {
completed: false,
message: `Process ${pid} (${process.name}) stopped.`
};
}
if (cmd === 'start' && parts.length > 1) {
const pid = parseInt(parts[1]);
const process = levelState.processes.find(p => p.pid === pid);
if (!process) {
return {
completed: false,
message: `No process with PID ${pid} found.`
};
}
if (process.status === 'running') {
return {
completed: false,
message: `Process ${pid} (${process.name}) is already running.`
};
}
// Start the process
process.status = 'running';
// Check if it was the firewall
if (process.name === 'firewall') {
levelState.firewallStarted = true;
// Check if level is completed
if (levelState.malwareKilled) {
return {
completed: true,
message: 'System secured! Malware stopped and firewall running.',
nextAction: 'next_level'
};
}
return {
completed: false,
message: `Started firewall process ${pid}. Now kill the malware!`
};
}
return {
completed: false,
message: `Process ${pid} (${process.name}) started.`
};
}
if (cmd === 'info' && parts.length > 1) {
const pid = parseInt(parts[1]);
const process = levelState.processes.find(p => p.pid === pid);
if (!process) {
return {
completed: false,
message: `No process with PID ${pid} found.`
};
}
let info = `Process Information:\n`;
info += `PID: ${process.pid}\n`;
info += `Name: ${process.name}\n`;
info += `CPU Usage: ${process.cpu.toFixed(1)}%\n`;
info += `Memory Usage: ${process.memory.toFixed(1)}%\n`;
info += `Status: ${process.status}\n`;
if (process.name === 'malware.bin') {
info += `\nWARNING: This process appears to be malicious!`;
} else if (process.name === 'firewall') {
info += `\nNOTE: This is the system's security service.`;
}
return {
completed: false,
message: info
};
}
return {
completed: false,
message: 'Unknown command or invalid syntax.'
};
},
hints: [
'Use "ps" to list all processes and their PIDs.',
'Look for processes with unusually high CPU or memory usage.',
'Use "kill [pid]" to stop a process and "start [pid]" to start one.',
'You need to both kill the malware and start the firewall to complete the level.'
]
};
export function registerLevel3() {
registerLevel(level);
}

304
src/levels/level4.ts Normal file
View File

@ -0,0 +1,304 @@
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
const level: Level = {
id: 4,
name: 'Permissions Puzzle',
description: 'Fix file permissions to access a protected file.',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
files: [
{ name: 'README.txt', permissions: 'rw-r--r--', owner: 'user', group: 'user' },
{ name: 'secret_data.db', permissions: '----------', owner: 'root', group: 'root' },
{ name: 'change_permissions.sh', permissions: 'r--------', owner: 'user', group: 'user' },
{ name: 'access_key.bin', permissions: 'rw-------', owner: 'root', group: 'user' }
],
currentUser: 'user',
sudoAvailable: false,
scriptExecutable: false,
accessKeyReadable: false
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
console.log('You need to access the protected files to proceed.');
console.log(`Current user: ${levelState.currentUser}`);
console.log('');
console.log('Files in current directory:');
console.log('PERMISSIONS OWNER GROUP FILENAME');
console.log('----------------------------------------');
levelState.files.forEach(file => {
console.log(
`${file.permissions} ${file.owner.padEnd(6)}${file.group.padEnd(7)}${file.name}`
);
});
console.log('');
console.log('Commands: "ls", "cat [file]", "chmod [permissions] [file]", "sudo [command]", "sh [script]"');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const command = input.trim();
// Split command into parts
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
if (cmd === 'ls') {
// Just show files again (same as render)
return {
completed: false,
message: 'File list displayed.'
};
}
if (cmd === 'cat' && parts.length > 1) {
const fileName = parts[1];
const file = levelState.files.find(f => f.name === fileName);
if (!file) {
return {
completed: false,
message: `File ${fileName} not found.`
};
}
// Check if user has read permission
const canRead = (levelState.currentUser === file.owner && file.permissions[0] === 'r') ||
(levelState.currentUser !== file.owner &&
levelState.currentUser === file.group && file.permissions[3] === 'r') ||
(levelState.currentUser !== file.owner &&
levelState.currentUser !== file.group && file.permissions[6] === 'r') ||
(levelState.currentUser === 'root'); // root can read anything
if (!canRead) {
return {
completed: false,
message: `Permission denied: Cannot read ${fileName}`
};
}
// Return file contents based on filename
if (fileName === 'README.txt') {
return {
completed: false,
message: `File contents:\n\nWelcome to the permissions puzzle!\n\nYou need to:\n1. Make the script executable\n2. Run the script to gain sudo access\n3. Access the protected data`
};
} else if (fileName === 'secret_data.db') {
return {
completed: true,
message: `File contents:\n\nCONGRATULATIONS!\nYou've successfully navigated the permissions puzzle and accessed the protected data.\n\nProceeding to next level...`,
nextAction: 'next_level'
};
} else if (fileName === 'change_permissions.sh') {
return {
completed: false,
message: `File contents:\n\n#!/bin/bash\n# This script grants sudo access\necho "Granting temporary sudo access..."\n# More script content here...`
};
} else if (fileName === 'access_key.bin') {
levelState.accessKeyReadable = true;
return {
completed: false,
message: `File contents:\n\nBINARY DATA: sudo_access_granted=true\n\nYou can now use sudo commands!`
};
}
}
if (cmd === 'chmod' && parts.length > 2) {
const permissions = parts[1];
const fileName = parts[2];
const file = levelState.files.find(f => f.name === fileName);
if (!file) {
return {
completed: false,
message: `File ${fileName} not found.`
};
}
// Check if user has permission to change permissions
const canModify = (levelState.currentUser === file.owner) ||
(levelState.currentUser === 'root');
if (!canModify) {
return {
completed: false,
message: `Permission denied: Cannot modify permissions of ${fileName}`
};
}
// Simple permission handling (just for the game)
if (permissions === '+x' || permissions === 'u+x') {
// Make executable for owner
const newPerms = file.permissions.split('');
newPerms[2] = 'x';
file.permissions = newPerms.join('');
if (fileName === 'change_permissions.sh') {
levelState.scriptExecutable = true;
}
return {
completed: false,
message: `Changed permissions of ${fileName} to ${file.permissions}`
};
} else if (permissions === '+r' || permissions === 'g+r') {
// Make readable for group
const newPerms = file.permissions.split('');
newPerms[3] = 'r';
file.permissions = newPerms.join('');
return {
completed: false,
message: `Changed permissions of ${fileName} to ${file.permissions}`
};
} else {
// For simplicity, just update the permissions string
file.permissions = permissions.length === 10 ? permissions : file.permissions;
return {
completed: false,
message: `Changed permissions of ${fileName} to ${file.permissions}`
};
}
}
if (cmd === 'sh' && parts.length > 1) {
const scriptName = parts[1];
const file = levelState.files.find(f => f.name === scriptName);
if (!file) {
return {
completed: false,
message: `Script ${scriptName} not found.`
};
}
// Check if script is executable
const canExecute = file.permissions[2] === 'x';
if (!canExecute) {
return {
completed: false,
message: `Permission denied: Cannot execute ${scriptName}. Make it executable first.`
};
}
if (scriptName === 'change_permissions.sh') {
levelState.sudoAvailable = true;
return {
completed: false,
message: `Executing ${scriptName}...\n\nGranting temporary sudo access...\nYou can now use sudo commands!`
};
}
return {
completed: false,
message: `Executed ${scriptName}, but nothing happened.`
};
}
if (cmd === 'sudo' && parts.length > 1) {
if (!levelState.sudoAvailable && !levelState.accessKeyReadable) {
return {
completed: false,
message: `sudo: command not found. You need to gain sudo access first.`
};
}
// Handle sudo commands
const sudoCmd = parts[1].toLowerCase();
if (sudoCmd === 'cat' && parts.length > 2) {
const fileName = parts[2];
const file = levelState.files.find(f => f.name === fileName);
if (!file) {
return {
completed: false,
message: `File ${fileName} not found.`
};
}
// With sudo, we can read any file
if (fileName === 'secret_data.db') {
return {
completed: true,
message: `File contents:\n\nCONGRATULATIONS!\nYou've successfully navigated the permissions puzzle and accessed the protected data.\n\nProceeding to next level...`,
nextAction: 'next_level'
};
} else {
return {
completed: false,
message: `File contents of ${fileName} displayed with sudo privileges.`
};
}
} else if (sudoCmd === 'chmod' && parts.length > 3) {
const permissions = parts[2];
const fileName = parts[3];
const file = levelState.files.find(f => f.name === fileName);
if (!file) {
return {
completed: false,
message: `File ${fileName} not found.`
};
}
// With sudo, we can change any permissions
file.permissions = permissions.length === 10 ? permissions : 'rw-r--r--';
return {
completed: false,
message: `Changed permissions of ${fileName} to ${file.permissions} with sudo privileges.`
};
}
}
return {
completed: false,
message: 'Unknown command or invalid syntax.'
};
},
hints: [
'First read the README.txt file to understand what you need to do.',
'You need to make the script executable with "chmod +x change_permissions.sh"',
'After making the script executable, run it with "sh change_permissions.sh"',
'Once you have sudo access, you can access any file with "sudo cat secret_data.db"',
'Alternatively, you can make the access_key.bin readable by your group with "chmod g+r access_key.bin"'
]
};
export function registerLevel4() {
registerLevel(level);
}

474
src/levels/level5.ts Normal file
View File

@ -0,0 +1,474 @@
import { Level, LevelResult, registerLevel } from '../core/levelSystem';
import { getCurrentGameState } from '../core/gameState';
const level: Level = {
id: 5,
name: 'Network Escape',
description: 'Configure network settings to escape the isolated system.',
async initialize() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Initialize level state if not already present
if (!gameState.levelStates[this.id]) {
gameState.levelStates[this.id] = {
interfaces: [
{ name: 'lo', status: 'UP', ip: '127.0.0.1', netmask: '255.0.0.0' },
{ name: 'eth0', status: 'DOWN', ip: '', netmask: '' },
{ name: 'wlan0', status: 'DOWN', ip: '', netmask: '' }
],
firewall: {
enabled: true,
rules: [
{ port: 22, protocol: 'tcp', action: 'DENY' },
{ port: 80, protocol: 'tcp', action: 'DENY' },
{ port: 443, protocol: 'tcp', action: 'DENY' },
{ port: 8080, protocol: 'tcp', action: 'DENY' }
]
},
dns: {
configured: false,
server: ''
},
gateway: {
configured: false,
address: ''
},
connections: [],
escapePortal: {
host: 'escape.portal',
ip: '10.0.0.1',
port: 8080
}
};
}
},
async render() {
const gameState = getCurrentGameState();
if (!gameState) return;
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
console.log('You\'re trapped in an isolated system. Configure the network to escape.');
console.log('');
console.log('Network Interfaces:');
console.log('NAME STATUS IP NETMASK');
console.log('----------------------------------------');
levelState.interfaces.forEach(iface => {
console.log(
`${iface.name.padEnd(7)}${iface.status.padEnd(9)}${iface.ip.padEnd(14)}${iface.netmask}`
);
});
console.log('');
console.log('Firewall Status: ' + (levelState.firewall.enabled ? 'ENABLED' : 'DISABLED'));
if (levelState.firewall.enabled) {
console.log('Firewall Rules:');
levelState.firewall.rules.forEach(rule => {
console.log(` ${rule.action} ${rule.protocol.toUpperCase()} port ${rule.port}`);
});
}
console.log('');
console.log('DNS Server: ' + (levelState.dns.configured ? levelState.dns.server : 'Not configured'));
console.log('Default Gateway: ' + (levelState.gateway.configured ? levelState.gateway.address : 'Not configured'));
console.log('');
console.log('Active Connections:');
if (levelState.connections.length === 0) {
console.log(' None');
} else {
levelState.connections.forEach(conn => {
console.log(` ${conn.protocol.toUpperCase()} ${conn.localAddress}:${conn.localPort} -> ${conn.remoteAddress}:${conn.remotePort}`);
});
}
console.log('');
console.log('Commands: "ifconfig", "ifup [interface]", "ifconfig [interface] [ip] [netmask]",');
console.log(' "firewall-cmd --list", "firewall-cmd --disable", "firewall-cmd --allow [port]",');
console.log(' "route add default [gateway]", "echo nameserver [ip] > /etc/resolv.conf",');
console.log(' "ping [host]", "nslookup [host]", "connect [host] [port]"');
},
async handleInput(input: string): Promise<LevelResult> {
const gameState = getCurrentGameState();
if (!gameState) {
return { completed: false };
}
// Make sure level state is initialized
if (!gameState.levelStates[this.id]) {
await this.initialize();
}
const levelState = gameState.levelStates[this.id];
const command = input.trim();
// Split command into parts
const parts = command.split(' ');
const cmd = parts[0].toLowerCase();
if (cmd === 'ifconfig') {
if (parts.length === 1) {
// Show all interfaces
return {
completed: false,
message: 'Network interfaces displayed.'
};
} else if (parts.length >= 4) {
// Configure an interface
const ifaceName = parts[1];
const ip = parts[2];
const netmask = parts[3];
const iface = levelState.interfaces.find(i => i.name === ifaceName);
if (!iface) {
return {
completed: false,
message: `Interface ${ifaceName} not found.`
};
}
if (iface.status === 'DOWN') {
return {
completed: false,
message: `Interface ${ifaceName} is down. Bring it up first with "ifup ${ifaceName}".`
};
}
// Simple IP validation
if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return {
completed: false,
message: `Invalid IP address format: ${ip}`
};
}
// Simple netmask validation
if (!netmask.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return {
completed: false,
message: `Invalid netmask format: ${netmask}`
};
}
// Set IP and netmask
iface.ip = ip;
iface.netmask = netmask;
return {
completed: false,
message: `Configured ${ifaceName} with IP ${ip} and netmask ${netmask}.`
};
}
}
if (cmd === 'ifup' && parts.length > 1) {
const ifaceName = parts[1];
const iface = levelState.interfaces.find(i => i.name === ifaceName);
if (!iface) {
return {
completed: false,
message: `Interface ${ifaceName} not found.`
};
}
if (iface.status === 'UP') {
return {
completed: false,
message: `Interface ${ifaceName} is already up.`
};
}
// Bring interface up
iface.status = 'UP';
return {
completed: false,
message: `Interface ${ifaceName} is now UP.`
};
}
if (cmd === 'firewall-cmd') {
if (parts.length > 1) {
const subCmd = parts[1];
if (subCmd === '--list') {
// List firewall rules
let message = 'Firewall rules:\n';
levelState.firewall.rules.forEach(rule => {
message += `${rule.action} ${rule.protocol.toUpperCase()} port ${rule.port}\n`;
});
return {
completed: false,
message
};
} else if (subCmd === '--disable') {
// Disable firewall
levelState.firewall.enabled = false;
return {
completed: false,
message: 'Firewall disabled.'
};
} else if (subCmd === '--allow' && parts.length > 2) {
// Allow a port
const port = parseInt(parts[2]);
if (isNaN(port) || port < 1 || port > 65535) {
return {
completed: false,
message: `Invalid port number: ${parts[2]}`
};
}
// Find the rule for this port
const ruleIndex = levelState.firewall.rules.findIndex(r => r.port === port);
if (ruleIndex >= 0) {
// Update existing rule
levelState.firewall.rules[ruleIndex].action = 'ALLOW';
} else {
// Add new rule
levelState.firewall.rules.push({
port,
protocol: 'tcp',
action: 'ALLOW'
});
}
return {
completed: false,
message: `Allowed TCP port ${port} through firewall.`
};
}
}
}
if (cmd === 'route' && parts[1] === 'add' && parts[2] === 'default' && parts.length > 3) {
const gateway = parts[3];
// Simple IP validation
if (!gateway.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return {
completed: false,
message: `Invalid gateway address format: ${gateway}`
};
}
// Set default gateway
levelState.gateway.configured = true;
levelState.gateway.address = gateway;
return {
completed: false,
message: `Default gateway set to ${gateway}.`
};
}
if (cmd === 'echo' && parts[1] === 'nameserver' && parts.length > 3 && parts[3] === '>' && parts[4] === '/etc/resolv.conf') {
const dnsServer = parts[2];
// Simple IP validation
if (!dnsServer.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return {
completed: false,
message: `Invalid DNS server address format: ${dnsServer}`
};
}
// Set DNS server
levelState.dns.configured = true;
levelState.dns.server = dnsServer;
return {
completed: false,
message: `DNS server set to ${dnsServer}.`
};
}
if (cmd === 'ping' && parts.length > 1) {
const host = parts[1];
// Check if we have a working network interface
const hasNetworkInterface = levelState.interfaces.some(iface =>
iface.status === 'UP' && iface.ip && iface.ip !== '127.0.0.1'
);
if (!hasNetworkInterface) {
return {
completed: false,
message: 'Network is unreachable. Configure a network interface first.'
};
}
// Check if we have a gateway configured
if (!levelState.gateway.configured) {
return {
completed: false,
message: 'Network is unreachable. Configure a default gateway first.'
};
}
// If pinging the escape portal
if (host === levelState.escapePortal.host) {
if (!levelState.dns.configured) {
return {
completed: false,
message: `ping: unknown host ${host}. Configure DNS first.`
};
}
return {
completed: false,
message: `PING ${host} (${levelState.escapePortal.ip}): 56 data bytes\n64 bytes from ${levelState.escapePortal.ip}: icmp_seq=0 ttl=64 time=0.1 ms\n64 bytes from ${levelState.escapePortal.ip}: icmp_seq=1 ttl=64 time=0.1 ms\n\n--- ${host} ping statistics ---\n2 packets transmitted, 2 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.1/0.1/0.1/0.0 ms`
};
} else if (host === levelState.escapePortal.ip) {
return {
completed: false,
message: `PING ${host}: 56 data bytes\n64 bytes from ${host}: icmp_seq=0 ttl=64 time=0.1 ms\n64 bytes from ${host}: icmp_seq=1 ttl=64 time=0.1 ms\n\n--- ${host} ping statistics ---\n2 packets transmitted, 2 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.1/0.1/0.1/0.0 ms`
};
}
return {
completed: false,
message: `ping: cannot resolve ${host}: Unknown host`
};
}
if (cmd === 'nslookup' && parts.length > 1) {
const host = parts[1];
if (!levelState.dns.configured) {
return {
completed: false,
message: `nslookup: can't resolve '${host}': No DNS servers configured`
};
}
if (host === levelState.escapePortal.host) {
return {
completed: false,
message: `Server:\t${levelState.dns.server}\nAddress:\t${levelState.dns.server}#53\n\nNon-authoritative answer:\nName:\t${host}\nAddress: ${levelState.escapePortal.ip}`
};
}
return {
completed: false,
message: `Server:\t${levelState.dns.server}\nAddress:\t${levelState.dns.server}#53\n\n** server can't find ${host}: NXDOMAIN`
};
}
if (cmd === 'connect' && parts.length > 2) {
const host = parts[1];
const port = parseInt(parts[2]);
if (isNaN(port) || port < 1 || port > 65535) {
return {
completed: false,
message: `Invalid port number: ${parts[2]}`
};
}
// Check if we have a working network interface
const hasNetworkInterface = levelState.interfaces.some(iface =>
iface.status === 'UP' && iface.ip && iface.ip !== '127.0.0.1'
);
if (!hasNetworkInterface) {
return {
completed: false,
message: 'Network is unreachable. Configure a network interface first.'
};
}
// Check if we have a gateway configured
if (!levelState.gateway.configured) {
return {
completed: false,
message: 'Network is unreachable. Configure a default gateway first.'
};
}
// Resolve host if needed
let resolvedIp = host;
if (host === levelState.escapePortal.host) {
if (!levelState.dns.configured) {
return {
completed: false,
message: `connect: could not resolve ${host}: Name or service not known`
};
}
resolvedIp = levelState.escapePortal.ip;
}
// Check if this is the escape portal
const isEscapePortal = (resolvedIp === levelState.escapePortal.ip && port === levelState.escapePortal.port);
// Check if firewall allows this connection
if (levelState.firewall.enabled) {
const rule = levelState.firewall.rules.find(r => r.port === port);
if (rule && rule.action === 'DENY') {
return {
completed: false,
message: `connect: Connection refused (blocked by firewall)`
};
}
}
if (isEscapePortal) {
// Success! Complete the level
return {
completed: true,
message: `Connected to escape portal at ${host}:${port}!\n\nWelcome to the escape portal. You have successfully configured the network and escaped the isolated system.\n\nCongratulations on completing all levels!`,
nextAction: 'main_menu'
};
}
// Add to connections list
levelState.connections.push({
protocol: 'tcp',
localAddress: levelState.interfaces.find(i => i.status === 'UP' && i.ip !== '127.0.0.1')?.ip || '0.0.0.0',
localPort: 12345 + levelState.connections.length,
remoteAddress: resolvedIp,
remotePort: port
});
return {
completed: false,
message: `Connected to ${host}:${port}, but nothing interesting happened.`
};
}
return {
completed: false,
message: 'Unknown command or invalid syntax.'
};
},
hints: [
'First bring up a network interface with "ifup eth0"',
'Configure the interface with "ifconfig eth0 10.0.0.2 255.255.255.0"',
'Set up a default gateway with "route add default 10.0.0.254"',
'Configure DNS with "echo nameserver 10.0.0.254 > /etc/resolv.conf"',
'Allow the escape portal port with "firewall-cmd --allow 8080"',
'Connect to the escape portal with "connect escape.portal 8080"'
]
};
export function registerLevel5() {
registerLevel(level);
}

150
src/ui/gameUI.ts Normal file
View File

@ -0,0 +1,150 @@
import { getCurrentGameState, saveGame } from '../core/gameState';
import { getLevelById, completeCurrentLevel, getAllLevels } from '../core/levelSystem';
import { renderMainMenu } from './mainMenu';
import { clearScreen, promptInput } from './uiHelpers';
import { styles, drawBox, drawTable } from './uiHelpers';
export async function renderGameUI(): Promise<void> {
const gameState = getCurrentGameState();
if (!gameState) {
console.error('No active game');
await renderMainMenu();
return;
}
// Game loop
while (true) {
// Get the current level at the start of each loop iteration
const currentLevel = getLevelById(gameState.currentLevel);
if (!currentLevel) {
console.error(styles.error(`Level ${gameState.currentLevel} not found`));
await renderMainMenu();
return;
}
clearScreen();
// Display game header
console.log(drawBox(
`TERMINAL ESCAPE - ${styles.title(currentLevel.name)}`,
`Player: ${styles.highlight(gameState.playerName)}\nLevel: ${gameState.currentLevel}/${getAllLevels().length}`
));
console.log('');
// Render current level
await currentLevel.render();
console.log('');
console.log('Available commands:');
console.log(`${styles.command('/help')} - Show help, ${styles.command('/save')} - Save game, ${styles.command('/menu')} - Main menu, ${styles.command('/hint')} - Get a hint`);
console.log('');
// Get player input
const input = await promptInput('');
// Handle special commands
if (input.startsWith('/')) {
const command = input.slice(1).toLowerCase();
if (command === 'help') {
await showHelp();
continue;
}
if (command === 'save') {
await saveGame();
console.log('Game saved successfully!');
await promptInput('Press Enter to continue...');
continue;
}
if (command === 'menu') {
await renderMainMenu();
return;
}
if (command === 'hint') {
await showHint(currentLevel.hints);
continue;
}
}
// Process level-specific input
const result = await currentLevel.handleInput(input);
if (result.message) {
console.log('');
console.log(result.message);
await promptInput('Press Enter to continue...');
}
if (result.completed) {
await completeCurrentLevel();
if (result.nextAction === 'main_menu') {
await renderMainMenu();
return;
} else if (result.nextAction === 'next_level') {
const nextLevelId = gameState.currentLevel + 1;
const nextLevel = getLevelById(nextLevelId);
if (nextLevel) {
gameState.currentLevel = nextLevelId;
// We don't need to reassign currentLevel here since we'll get it at the start of the next loop
} else {
// Game completed
clearScreen();
console.log('Congratulations! You have completed all levels!');
await promptInput('Press Enter to return to the main menu...');
await renderMainMenu();
return;
}
}
}
}
}
async function showHelp(): Promise<void> {
clearScreen();
console.log('=== Help ===');
console.log('');
console.log('Terminal Escape is a puzzle game where you solve Linux-themed challenges.');
console.log('');
console.log('Special Commands:');
console.log('/help - Show this help screen');
console.log('/save - Save your game');
console.log('/menu - Return to main menu');
console.log('/hint - Get a hint for the current level');
console.log('');
console.log('Each level has its own commands and puzzles to solve.');
console.log('');
await promptInput('Press Enter to continue...');
}
async function showHint(hints: string[]): Promise<void> {
const gameState = getCurrentGameState();
if (!gameState) return;
// Get level state for hints
const levelState = gameState.levelStates[gameState.currentLevel] || {};
const hintIndex = levelState.hintIndex || 0;
clearScreen();
console.log('=== Hint ===');
console.log('');
if (hintIndex < hints.length) {
console.log(hints[hintIndex]);
// Update hint index for next time
gameState.levelStates[gameState.currentLevel] = {
...levelState,
hintIndex: hintIndex + 1
};
} else {
console.log('No more hints available for this level.');
}
console.log('');
await promptInput('Press Enter to continue...');
}

51
src/ui/levelRenderer.ts Normal file
View File

@ -0,0 +1,51 @@
import { styles, drawBox, drawTable } from './uiHelpers';
export const levelUI = {
title: (text: string) => console.log(styles.title(text)),
subtitle: (text: string) => console.log(styles.subtitle(text)),
paragraph: (text: string) => console.log(text),
spacer: () => console.log(''),
box: (title: string, content: string) => console.log(drawBox(title, content)),
terminal: (content: string) => {
console.log(' ' + styles.dim('┌─ Terminal ───────────────────────┐'));
content.split('\n').forEach(line => {
console.log(' ' + styles.dim('│') + ' ' + styles.highlight(line));
});
console.log(' ' + styles.dim('└────────────────────────────────────┘'));
},
fileSystem: (path: string, items: {name: string, type: string}[]) => {
console.log(styles.path(`Current directory: ${path}`));
console.log('');
if (items.length === 0) {
console.log(styles.dim(' (empty directory)'));
return;
}
items.forEach(item => {
const icon = item.type === 'dir' ? '📁' : '📄';
console.log(` ${icon} ${item.name}`);
});
},
processTable: (processes: any[]) => {
const headers = ['PID', 'NAME', 'CPU%', 'MEM%', 'STATUS'];
const rows = processes.map(p => [
p.pid.toString(),
p.name,
p.cpu.toFixed(1),
p.memory.toFixed(1),
p.status
]);
console.log(drawTable(headers, rows));
},
commands: (commands: string[]) => {
console.log(styles.subtitle('Available Commands:'));
commands.forEach(cmd => console.log(' ' + styles.command(cmd)));
}
};

146
src/ui/mainMenu.ts Normal file
View File

@ -0,0 +1,146 @@
import { createNewGame, loadGame } from '../core/gameState';
import { listSaves } from '../core/gameInit';
import { getLeaderboard, formatTime } from '../core/leaderboard';
import { startLevel, getAllLevels } from '../core/levelSystem';
import { renderGameUI } from './gameUI';
import { clearScreen, promptInput, styles, drawBox } from './uiHelpers';
import kleur from 'kleur';
// ASCII art logo
const LOGO = `
`;
export async function renderMainMenu(): Promise<void> {
while (true) {
clearScreen();
console.log(kleur.cyan(LOGO));
console.log(styles.subtitle('A Linux Terminal Escape Room Game'));
console.log('');
const menuOptions = [
'1. ' + styles.command('New Game'),
'2. ' + styles.command('Load Game'),
'3. ' + styles.command('Leaderboard'),
'4. ' + styles.command('Exit')
];
console.log(drawBox('MAIN MENU', menuOptions.join('\n')));
console.log('');
const choice = await promptInput('Select an option: ');
switch (choice) {
case '1':
await newGameMenu();
break;
case '2':
await loadGameMenu();
break;
case '3':
await showLeaderboard();
break;
case '4':
console.log('Thanks for playing!');
process.exit(0);
default:
console.log('Invalid option. Press Enter to continue...');
await promptInput('');
}
}
}
async function newGameMenu(): Promise<void> {
clearScreen();
console.log('=== New Game ===');
console.log('');
const playerName = await promptInput('Enter your name: ');
if (!playerName) {
console.log('Name cannot be empty. Press Enter to return to main menu...');
await promptInput('');
return;
}
// Create new game state
createNewGame(playerName);
// Start the first level
const levels = getAllLevels();
if (levels.length > 0) {
await startLevel(levels[0].id);
await renderGameUI();
} else {
console.log('No levels available. Press Enter to return to main menu...');
await promptInput('');
}
}
async function loadGameMenu(): Promise<void> {
clearScreen();
console.log('=== Load Game ===');
console.log('');
const saves = await listSaves();
if (saves.length === 0) {
console.log('No saved games found. Press Enter to return to main menu...');
await promptInput('');
return;
}
console.log('Available saves:');
saves.forEach((save, index) => {
console.log(`${index + 1}. ${save.replace('.json', '')}`);
});
console.log('');
const choice = await promptInput('Select a save to load (or 0 to cancel): ');
const choiceNum = parseInt(choice);
if (choiceNum === 0 || isNaN(choiceNum) || choiceNum > saves.length) {
return;
}
const saveName = saves[choiceNum - 1].replace('.json', '');
const success = await loadGame(saveName);
if (success) {
await renderGameUI();
} else {
console.log('Failed to load game. Press Enter to return to main menu...');
await promptInput('');
}
}
async function showLeaderboard(): Promise<void> {
clearScreen();
console.log('=== Leaderboard ===');
console.log('');
const leaderboard = await getLeaderboard();
if (leaderboard.players.length === 0) {
console.log('No entries yet. Be the first to complete the game!');
} else {
console.log('Top Players:');
console.log('-----------');
leaderboard.players.slice(0, 10).forEach((entry, index) => {
console.log(`${index + 1}. ${entry.playerName} - ${formatTime(entry.completionTime)}`);
});
}
console.log('');
await promptInput('Press Enter to return to main menu...');
}

20
src/ui/soundEffects.ts Normal file
View File

@ -0,0 +1,20 @@
import player from 'play-sound';
const audioPlayer = player({});
export function playSound(sound: 'success' | 'error' | 'typing' | 'levelComplete'): void {
try {
const soundMap = {
success: 'sounds/success.wav',
error: 'sounds/error.wav',
typing: 'sounds/typing.wav',
levelComplete: 'sounds/level-complete.wav'
};
audioPlayer.play(soundMap[sound], (err) => {
if (err) console.error('Error playing sound:', err);
});
} catch (error) {
// Silently fail if sound can't be played
}
}

79
src/ui/uiHelpers.ts Normal file
View File

@ -0,0 +1,79 @@
import readline from 'readline';
import kleur from 'kleur';
// Enable colors
kleur.enabled = true;
export function clearScreen(): void {
console.clear();
}
export function promptInput(prompt: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise<string>(resolve => {
rl.question(kleur.green('> ') + prompt, answer => {
rl.close();
resolve(answer);
});
});
}
// Add styled text helpers
export const styles = {
title: (text: string) => kleur.bold().cyan(text),
subtitle: (text: string) => kleur.bold().blue(text),
success: (text: string) => kleur.bold().green(text),
error: (text: string) => kleur.bold().red(text),
warning: (text: string) => kleur.bold().yellow(text),
info: (text: string) => kleur.bold().magenta(text),
command: (text: string) => kleur.bold().yellow(text),
path: (text: string) => kleur.italic().white(text),
highlight: (text: string) => kleur.bold().white(text),
dim: (text: string) => kleur.dim().white(text)
};
// Add box drawing functions
export function drawBox(title: string, content: string): string {
const lines = content.split('\n');
const width = Math.max(title.length + 4, ...lines.map(line => line.length + 4));
let result = '╔' + '═'.repeat(width - 2) + '╗\n';
result += '║ ' + kleur.bold().cyan(title) + ' '.repeat(width - title.length - 3) + '║\n';
result += '╠' + '═'.repeat(width - 2) + '╣\n';
lines.forEach(line => {
result += '║ ' + line + ' '.repeat(width - line.length - 3) + '║\n';
});
result += '╚' + '═'.repeat(width - 2) + '╝';
return result;
}
export function drawTable(headers: string[], rows: string[][]): string {
// Calculate column widths
const colWidths = headers.map((h, i) =>
Math.max(h.length, ...rows.map(row => row[i]?.length || 0)) + 2
);
// Create separator line
const separator = '┼' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┼';
// Create header
let result = '┌' + colWidths.map(w => '─'.repeat(w)).join('┬') + '┐\n';
result += '│' + headers.map((h, i) => kleur.bold().white(h.padEnd(colWidths[i]))).join('│') + '│\n';
result += '├' + separator.substring(1, separator.length - 1) + '┤\n';
// Create rows
rows.forEach(row => {
result += '│' + row.map((cell, i) => cell.padEnd(colWidths[i])).join('│') + '│\n';
});
// Create footer
result += '└' + colWidths.map(w => '─'.repeat(w)).join('┴') + '┘';
return result;
}

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"types": ["bun-types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}