UNPKG

rpg-mcp-server

Version:

LLM-driven RPG game server using MCP protocol

219 lines (218 loc) 7 kB
import { randomUUID } from 'crypto'; /** * 게임 관리자 클래스 * 게임 생성, 업데이트, 조회를 담당 */ export class GameManager { games = new Map(); /** * 새 게임 생성 */ createGame(initialState) { const gameId = randomUUID(); const now = new Date(); const game = { gameId, state: initialState, createdAt: now, updatedAt: now, }; this.games.set(gameId, game); console.error(`Game created with ID: ${gameId}`); return { game, nextActions: ['progressStory'], }; } /** * 게임 상태 업데이트 */ updateGame(gameId, fieldSelector, value) { const game = this.games.get(gameId); if (!game) { throw new Error(`Game with id ${gameId} not found`); } // 게임 상태의 깊은 복사본 생성 const newState = JSON.parse(JSON.stringify(game.state)); // 경로에서 'game.' 접두사 제거 (호환성) const cleanPath = fieldSelector.replace(/^game\./, ''); // 중첩된 값 설정 this.setNestedValue(newState, cleanPath, value); // 게임 업데이트 game.state = newState; game.updatedAt = new Date(); console.error(`Game ${gameId} updated: ${fieldSelector} = ${JSON.stringify(value)}`); return { game, nextActions: ['progressStory'], }; } /** * 게임 조회 */ getGame(gameId) { const game = this.games.get(gameId); if (!game) { throw new Error(`Game with id ${gameId} not found`); } return { game, nextActions: [], }; } selectAction(gameId, selectedOption, selectedIndex) { const game = this.games.get(gameId); if (!game) { throw new Error(`Game with id ${gameId} not found`); } // 현재 진행 중인 상황과 선택지를 히스토리에서 확인 if (!game.state.lastStoryProgress) { throw new Error(`No current situation available for selection`); } // 게임 히스토리에 상황-액션 쌍 추가 this.addToGameHistory(game, selectedOption, selectedIndex); // 선택된 액션을 게임 상태에 반영 game.state.selectedAction = { option: selectedOption, index: selectedIndex, timestamp: new Date() }; game.updatedAt = new Date(); return { game, nextActions: ["updateGame"] // 다음 업데이트로 체인 연결 }; } /** * 스토리 진행 */ progressStory(gameId, progress) { const game = this.games.get(gameId); if (!game) { throw new Error(`Game with id ${gameId} not found`); } // progress 파라미터를 활용해 스토리 진행 상황을 기록하거나 반영할 수 있음 if (!game.state.story) { game.state.story = { progress }; } else { game.state.story.progress = progress; } game.state.lastStoryProgress = progress; game.updatedAt = new Date(); console.error(`Game ${gameId} story progressed: ${progress}`); return { game, nextActions: ['promptUserActions'], }; } /** * 사용자 액션 프롬프트 */ promptUserActions(gameId, options) { const game = this.games.get(gameId); if (!game) { throw new Error(`Game with id ${gameId} not found`); } // options 파라미터를 활용해 유저에게 선택지를 제시 // 게임 상태와 분리된 메타데이터로 저장하는 것이 더 안전할 수 있음 game.state._currentOptions = options; game.updatedAt = new Date(); console.error(`Game ${gameId} prompting user actions: ${JSON.stringify(options)}`); return { game, nextActions: [], }; } /** * 중첩된 객체의 값을 설정하는 헬퍼 메서드 */ setNestedValue(obj, path, value) { const keys = this.parsePath(path); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current)) { // 다음 키가 숫자인지 확인하여 배열 또는 객체 생성 const nextKey = keys[i + 1]; current[key] = /^\d+$/.test(nextKey) ? [] : {}; } current = current[key]; } current[keys[keys.length - 1]] = value; } /** * 경로를 파싱하여 키 배열로 변환 * 예: "characters[0].name" -> ["characters", "0", "name"] */ parsePath(path) { const result = []; let current = ''; let inBrackets = false; for (let i = 0; i < path.length; i++) { const char = path[i]; if (char === '[') { if (current) { result.push(current); current = ''; } inBrackets = true; } else if (char === ']') { if (current) { result.push(current); current = ''; } inBrackets = false; } else if (char === '.' && !inBrackets) { if (current) { result.push(current); current = ''; } } else { current += char; } } if (current) { result.push(current); } return result; } /** * 게임 히스토리에 상황-액션 쌍을 추가 (최대 10개 유지) */ addToGameHistory(game, selectedOption, selectedIndex) { if (!game.state._gameHistory) { game.state._gameHistory = []; } if (!game.state.lastStoryProgress || !game.state._currentOptions) { return; // 필요한 정보가 없으면 추가하지 않음 } const historyEntry = { situation: game.state.lastStoryProgress, options: [...game.state._currentOptions], // 배열 복사 selectedOption, selectedIndex, timestamp: new Date() }; game.state._gameHistory.push(historyEntry); // 최대 10개만 유지 if (game.state._gameHistory.length > 10) { game.state._gameHistory = game.state._gameHistory.slice(-10); } } /** * 모든 게임 목록 조회 (디버깅용) */ getAllGames() { return Array.from(this.games.values()); } /** * 게임 삭제 (정리용) */ deleteGame(gameId) { return this.games.delete(gameId); } }