293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Test Server for AI Interactive Fiction
|
|
* Simplified version that sends test paragraphs instead of using LLM
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.io = exports.server = exports.app = void 0;
|
|
const path_1 = __importDefault(require("path"));
|
|
const express_1 = __importDefault(require("express"));
|
|
const http_1 = __importDefault(require("http"));
|
|
const socket_io_1 = require("socket.io");
|
|
const dotenv = __importStar(require("dotenv"));
|
|
const fs_1 = require("fs");
|
|
const turn_result_1 = require("./interfaces/turn-result");
|
|
// Load environment variables
|
|
dotenv.config();
|
|
// Create Express application
|
|
const app = (0, express_1.default)();
|
|
exports.app = app;
|
|
const server = http_1.default.createServer(app);
|
|
exports.server = server;
|
|
const io = new socket_io_1.Server(server);
|
|
exports.io = io;
|
|
// Get port from environment variables or use default
|
|
const DEFAULT_PORT = 3001;
|
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT;
|
|
const PORT_RANGE = 300; // Try enough ports to skip OS-excluded ranges.
|
|
// Serve static files from the public directory. Keep browser modules uncached
|
|
// during local development so fixes are visible without a hard cache clear.
|
|
app.use(express_1.default.static(path_1.default.join(__dirname, '../public'), {
|
|
etag: false,
|
|
lastModified: false,
|
|
setHeaders: (res) => {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
}
|
|
}));
|
|
// Test paragraphs to send to the client
|
|
const TEST_PARAGRAPHS = [
|
|
"You stand at the entrance of a mysterious cave. The air is cool and damp, carrying the scent of earth and ancient stone. Shadows dance on the walls as your torch flickers in the gentle breeze.",
|
|
"As you venture deeper, the passage narrows. Stalactites hang from the ceiling like stone daggers, their surfaces glistening with moisture. The sound of dripping water echoes through the silence.",
|
|
"Suddenly, the passage opens into a vast chamber. Crystal formations catch the light of your torch, sending rainbow reflections across the walls. In the center of the room stands an ancient stone pedestal, its surface carved with symbols from a forgotten language."
|
|
];
|
|
// Handle socket connections
|
|
io.on('connection', (socket) => {
|
|
console.log(`New client connected: ${socket.id}`);
|
|
let currentParagraphIndex = 0;
|
|
let gameRunning = false;
|
|
let nextTurnId = 1;
|
|
const saveGames = new Set();
|
|
const startDemoGame = () => {
|
|
gameRunning = true;
|
|
nextTurnId = 1;
|
|
currentParagraphIndex = 0;
|
|
socket.emit('narrativeResponse', {
|
|
turnId: nextTurnId++,
|
|
paragraphs: [
|
|
...(0, turn_result_1.textToParagraphs)("#chapter[Interactive Fiction Test]\n\nWelcome to the Interactive Fiction Test. This is a simplified version that sends predefined paragraphs instead of using an LLM."),
|
|
...(0, turn_result_1.textToParagraphs)(TEST_PARAGRAPHS[0]),
|
|
],
|
|
choices: [],
|
|
inputMode: 'text',
|
|
gameState: {
|
|
currentRoomId: 'test-room',
|
|
},
|
|
});
|
|
};
|
|
const normalizeSaveSlot = (slot) => {
|
|
const value = Number(slot);
|
|
return Number.isInteger(value) && value > 0 ? value : 1;
|
|
};
|
|
socket.on('gameApi', (request, respond) => {
|
|
try {
|
|
const method = String(request?.method || '');
|
|
const args = Array.isArray(request?.args) ? request.args : [];
|
|
let response;
|
|
switch (method) {
|
|
case 'newGame':
|
|
case 'newGame()':
|
|
startDemoGame();
|
|
response = { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
|
|
break;
|
|
case 'loadGame':
|
|
case 'loadGame()': {
|
|
const slot = normalizeSaveSlot(args[0]);
|
|
if (!saveGames.has(slot)) {
|
|
response = { success: false, error: 'missing_save', result: false };
|
|
break;
|
|
}
|
|
startDemoGame();
|
|
socket.emit('gameLoaded', { slot });
|
|
response = { success: true, result: true, running: true, slot };
|
|
break;
|
|
}
|
|
case 'saveGame':
|
|
case 'saveGame()': {
|
|
if (!gameRunning) {
|
|
response = { success: false, error: 'game_not_running', result: false };
|
|
break;
|
|
}
|
|
const slot = normalizeSaveSlot(args[0]);
|
|
saveGames.add(slot);
|
|
socket.emit('gameSaved', { slot });
|
|
response = { success: true, result: true, slot };
|
|
break;
|
|
}
|
|
case 'hasSaveGame':
|
|
case 'hasSaveGame()': {
|
|
const slot = normalizeSaveSlot(args[0]);
|
|
response = { success: true, result: saveGames.has(slot), slot };
|
|
break;
|
|
}
|
|
case 'getSaveGames':
|
|
case 'getSaveGames()':
|
|
response = { success: true, result: Array.from(saveGames).sort((a, b) => a - b) };
|
|
break;
|
|
case 'isGameRunning':
|
|
case 'isGameRunning()':
|
|
response = { success: true, result: gameRunning };
|
|
break;
|
|
default:
|
|
response = { success: false, error: `unknown_method:${method}` };
|
|
}
|
|
if (typeof respond === 'function')
|
|
respond(response);
|
|
}
|
|
catch (error) {
|
|
if (typeof respond === 'function') {
|
|
respond({ success: false, error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
}
|
|
});
|
|
// Start a new game
|
|
socket.on('startGame', async () => {
|
|
try {
|
|
console.log('Starting test game session');
|
|
startDemoGame();
|
|
}
|
|
catch (error) {
|
|
console.error('Error starting game:', error);
|
|
socket.emit('error', { message: 'Failed to start game. Please try again.' });
|
|
}
|
|
});
|
|
// Process player command
|
|
socket.on('playerCommand', async (data) => {
|
|
try {
|
|
console.log(`Received command: ${data.command}`);
|
|
// Send narrative response to client
|
|
socket.emit('narrativeResponse', {
|
|
turnId: nextTurnId++,
|
|
paragraphs: (0, turn_result_1.textToParagraphs)(String(data.command || '')),
|
|
choices: [],
|
|
inputMode: 'text',
|
|
gameState: {
|
|
currentRoomId: "test-room"
|
|
},
|
|
suggestions: ["look around", "examine pedestal", "touch crystals"]
|
|
});
|
|
}
|
|
catch (error) {
|
|
console.error('Error processing command:', error);
|
|
socket.emit('error', { message: 'Failed to process command. Please try again.' });
|
|
}
|
|
});
|
|
// Handle disconnection
|
|
socket.on('disconnect', () => {
|
|
console.log(`Client disconnected: ${socket.id}`);
|
|
});
|
|
});
|
|
// Ensure required asset folders exist
|
|
function ensureDirectories() {
|
|
const dirs = [
|
|
path_1.default.join(__dirname, '../public'),
|
|
path_1.default.join(__dirname, '../public/js'),
|
|
path_1.default.join(__dirname, '../public/css'),
|
|
path_1.default.join(__dirname, '../public/images'),
|
|
path_1.default.join(__dirname, '../public/music'),
|
|
path_1.default.join(__dirname, '../public/sounds'),
|
|
path_1.default.join(__dirname, '../public/fonts')
|
|
];
|
|
for (const dir of dirs) {
|
|
if (!(0, fs_1.existsSync)(dir)) {
|
|
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
}
|
|
}
|
|
}
|
|
// Copy kokoro-js library from node_modules if not already present
|
|
function ensureKokoroJs() {
|
|
const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js');
|
|
const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js');
|
|
if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) {
|
|
(0, fs_1.copyFileSync)(source, destination);
|
|
console.log(`Copied kokoro-js from ${source} to ${destination}`);
|
|
}
|
|
}
|
|
// Start the server with port fallback
|
|
async function startServer(initialPort, range) {
|
|
let currentPort = initialPort;
|
|
const maxPort = initialPort + range;
|
|
// Try ports in the specified range
|
|
while (currentPort < maxPort) {
|
|
try {
|
|
// Ensure directories exist
|
|
ensureDirectories();
|
|
// Ensure kokoro-js is copied
|
|
try {
|
|
ensureKokoroJs();
|
|
}
|
|
catch (error) {
|
|
console.error('Error copying kokoro-js:', error);
|
|
}
|
|
// Try to start the server on the current port
|
|
await new Promise((resolve, reject) => {
|
|
server.removeAllListeners('error');
|
|
server.removeAllListeners('listening');
|
|
server.once('listening', () => {
|
|
console.log(`AI Interactive Fiction TEST SERVER running on http://localhost:${currentPort}`);
|
|
console.log('This server is sending predefined test paragraphs instead of using an LLM');
|
|
resolve();
|
|
});
|
|
server.once('error', (error) => {
|
|
// If port is in use, try next port
|
|
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') {
|
|
console.log(`Port ${currentPort} is unavailable (${error.code}), trying next port...`);
|
|
server.close();
|
|
currentPort++;
|
|
reject();
|
|
}
|
|
else {
|
|
// For other errors, log and reject
|
|
console.error('Server error:', error);
|
|
reject(error);
|
|
}
|
|
});
|
|
server.listen(currentPort);
|
|
});
|
|
// If we reach here, server started successfully
|
|
return;
|
|
}
|
|
catch (error) {
|
|
// If we reach the max port and still fail, throw an error
|
|
if (currentPort >= maxPort - 1) {
|
|
throw new Error(`Failed to start server on ports ${initialPort} to ${maxPort - 1}`);
|
|
}
|
|
// Otherwise try the next port
|
|
}
|
|
}
|
|
}
|
|
// Start the server when this module is run directly
|
|
if (require.main === module) {
|
|
startServer(PORT, PORT_RANGE).catch(error => {
|
|
console.error('Failed to start server:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
//# sourceMappingURL=test-server.js.map
|