This commit is contained in:
Kim Ravn Hansen
2025-09-05 13:38:17 +02:00
parent 438f4fe59e
commit 3835ad1de3
9 changed files with 613 additions and 622 deletions

View File

@@ -1,194 +1,73 @@
const WebSocket = require('ws');
const http = require('http');
const path = require('path');
const fs = require('fs');
import WebSocket, { WebSocketServer } from "ws";
import http from "http";
import path from "path";
import fs from "fs";
import { Player } from "./models/player.js";
import { Game } from "./models/game.js";
/**
* Player
* @property WebSocket websocket
* Parse a string with json-encoded data without throwing exceptions.
*
* @param {string} data
* @return {any}
*/
class Player {
/**
*
* @param {String} name
* @param {WebSocket} websocket
*/
constructor(name, websocket) {
this.name = name;
this.websocket = websocket;
this.currentRoom = 'town_square';
this.health = 100;
this.inventory = [];
this.level = 1;
function parseJson(data) {
if (typeof data !== "string") {
console.error("Attempting to parse json, but data was not even a string", data);
return;
}
/***
* Send a message back to the client via the websocket.
*
* @param {string} message
*/
sendMessage(message) {
if (this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify({
type: 'message',
content: message
}));
}
}
sendPrompt() {
this.sendMessage(`\n[${this.currentRoom}] > `);
}
}
class Room {
/**
*
* @param {string} id
* @param {string} name
* @param {string} description
* @param {string[]} exits
*/
constructor(id, name, description, exits = {}) {
this.id = id;
this.name = name;
this.description = description;
this.exits = exits; // { north: 'room_id', south: 'room_id' }
this.players = new Set();
this.items = [];
this.npcs = [];
}
/**
* Add a player to the list of active players.
*
* (an active player is a player that currently has an active web socketA)
*
* @param {Player} player
*/
addPlayer(player) {
this.players.add(player);
this.broadcastToRoom(`${player.name} enters the room.`, player);
}
/**
* Remove a player from the list of active players.
*
* (an active player is a player that currently has an active web socketA)
*
* @param {Player} player
*/
removePlayer(player) {
this.players.delete(player);
this.broadcastToRoom(`${player.name} leaves the room.`, player);
}
/**
* Send a message to all other players in this room.
*
* @param {string} message
* @param {Player} excludePlayer A single player to exclude from the broadcast
*/
broadcastToRoom(message, excludePlayer = null) {
// for (const player of this.players) {
// if (player !== excludePlayer) {
// player.sendMessage(message);
// }
// }
this.getPlayersExcept(excludePlayer).forEach((player) => {
player.sendMessage(message);
})
}
getPlayersExcept(excludePlayer) {
return Array.from(this.players).filter(p => p !== excludePlayer);
try {
return JSON.parse(data)
} catch (error) {
console.error('Error parsing data as json:', error, data);
}
}
class MudServer {
constructor() {
this.players = new Map(); // websocket -> Player
this.rooms = new Map();
this.playersByName = new Map(); // name -> Player
this.initializeRooms();
}
initializeRooms() {
const townSquare = new Room(
'town_square',
'Town Square',
'You are standing in the bustling town square. A fountain sits in the center, and cobblestone paths lead in all directions. The inn lies to the north, and a mysterious forest path leads east.',
{ north: 'inn', east: 'forest_entrance' }
);
const inn = new Room(
'inn',
'The Rusty Dragon Inn',
'A cozy tavern filled with the aroma of hearty stew and ale. Adventurers gather around wooden tables, sharing tales of their exploits.',
{ south: 'town_square' }
);
const forestEntrance = new Room(
'forest_entrance',
'Forest Entrance',
'The edge of a dark, mysterious forest. Ancient trees tower overhead, and you can hear strange sounds echoing from within.',
{ west: 'town_square', north: 'deep_forest' }
);
const deepForest = new Room(
'deep_forest',
'Deep Forest',
'You are deep within the forest. Shadows dance between the trees, and you feel like you\'re being watched.',
{ south: 'forest_entrance' }
);
this.rooms.set('town_square', townSquare);
this.rooms.set('inn', inn);
this.rooms.set('forest_entrance', forestEntrance);
this.rooms.set('deep_forest', deepForest);
this.game = new Game();
}
/**
*
* @param {WebSocket} ws
*/
handleConnection(ws) {
onConnectionEstabished(ws) {
console.log('New connection established');
ws.send(JSON.stringify({
type: 'message',
content: 'Welcome to the WebSocket MUD!\nWhat is your character name?'
}));
ws.send(JSON.stringify(
["m", "Welcome to the WebSocket MUD!\nWhat is your username name?"]
));
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
this.handleMessage(ws, message);
} catch (error) {
console.error('Error parsing message:', error);
}
this.onIncomingMessage(parseJson(data));
});
ws.on('close', () => {
this.handleDisconnection(ws);
this.onConnectionClosed(ws);
});
}
/**
*
* @param {WebSocket} ws
* @param {strings} message
* @returns
*/
handleMessage(ws, message) {
onIncomingMessage(ws, message) {
const player = this.players.get(ws);
if (!player) {
// Player hasn't been created yet, expecting name
const name = message.content.trim();
if (name && !this.playersByName.has(name)) {
if (name && !this.players.has(name)) {
this.createPlayer(ws, name);
} else {
/**
* @todo: send an array instead of object.
* element 1 is the type
* element 2 is the content
* element 3+ are expansions
*/
ws.send(JSON.stringify({
type: 'message',
content: 'Invalid name or name already taken. Please choose another:'
@@ -209,9 +88,9 @@ class MudServer {
createPlayer(ws, name) {
const player = new Player(name, ws);
this.players.set(ws, player);
this.playersByName.set(name, player);
this.players.set(name, player);
const startRoom = this.rooms.get(player.currentRoom);
const startRoom = this.rooms.get("town_square");
startRoom.addPlayer(player);
player.sendMessage(`Welcome, ${name}! You have entered the world.`);
@@ -354,7 +233,7 @@ class MudServer {
}
showOnlinePlayers(player) {
const playerList = Array.from(this.playersByName.keys());
const playerList = Array.from(this.players.keys());
player.sendMessage(`Online players (${playerList.length}): ${playerList.join(', ')}`);
}
@@ -380,7 +259,10 @@ Available Commands:
player.sendMessage(helpText);
}
handleDisconnection(ws) {
/**
* Called when a websocket connection is closing.
*/
onConnectionClosed(ws) {
const player = this.players.get(ws);
if (player) {
console.log(`Player ${player.name} disconnected`);
@@ -393,14 +275,15 @@ Available Commands:
// Clean up references
this.players.delete(ws);
this.playersByName.delete(player.name);
this.players.delete(player.name);
}
}
}
// Create HTTP server for serving the client
const server = http.createServer((req, res) => {
let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);
// let filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);
let filePath = path.join('public', req.url === '/' ? 'index.html' : req.url);
const ext = path.extname(filePath);
const contentTypes = {
@@ -409,13 +292,15 @@ const server = http.createServer((req, res) => {
'.html': 'text/html',
};
if (!contentType[ext]) {
if (!contentTypes[ext]) {
// Invalid file, pretend it did not exist!
res.writeHead(404);
res.end('File not found');
return;
}
const contentType = contentTypes[ext];
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
@@ -428,11 +313,11 @@ const server = http.createServer((req, res) => {
});
// Create WebSocket server
const wss = new WebSocket.Server({ server });
const wss = new WebSocketServer({ server });
const mudServer = new MudServer();
wss.on('connection', (ws) => {
mudServer.handleConnection(ws);
mudServer.onConnectionEstabished(ws);
});
const PORT = process.env.PORT || 3000;