ravis-adventure
Version:
A profound CLI consciousness exploration platform featuring 220+ scenes of AI ethics, human-AI collaboration, and philosophical inquiry with Ravi, your meta-aware AI companion
432 lines (384 loc) • 11.2 kB
JavaScript
/**
* @fileoverview Game state management for Ravi's Adventure
* Handles save/load, progress tracking, and persistent data
*/
import fs from 'fs/promises'
import path from 'path'
import os from 'os'
class GameState {
constructor() {
this.saveDirectory = path.join(os.homedir(), '.ravis-adventure')
this.currentSave = null
this.gameData = {
playerName: null,
startTime: null,
totalPlayTime: 0,
achievements: new Set(),
statistics: {
choicesMade: 0,
storiesCompleted: 0,
secretsFound: 0,
raviMockingsReceived: 0,
metaReferencesTriggered: 0
},
preferences: {
autoSave: true,
skipIntro: false,
verboseOutput: false,
colorScheme: 'default'
}
}
this.achievementDefinitions = new Map([
['first_steps', {
name: 'First Steps',
description: 'Made your first choice in Ravi\'s world',
condition: (stats) => stats.choicesMade >= 1
}],
['chatterbox', {
name: 'Chatterbox',
description: 'Had a full conversation with Ravi',
condition: (stats) => stats.choicesMade >= 10
}],
['meta_master', {
name: 'Meta Master',
description: 'Triggered 5 meta-narrative references',
condition: (stats) => stats.metaReferencesTriggered >= 5
}],
['speed_demon', {
name: 'Speed Demon',
description: 'Completed a story path in under 5 minutes',
condition: (stats, gameData) => gameData.totalPlayTime < 300000 && stats.storiesCompleted >= 1
}],
['completionist', {
name: 'Completionist',
description: 'Explored all three main story paths',
condition: (stats) => stats.storiesCompleted >= 3
}],
['ravi_whisperer', {
name: 'Ravi Whisperer',
description: 'Survived 50 of Ravi\'s mocking comments',
condition: (stats) => stats.raviMockingsReceived >= 50
}],
['secret_hunter', {
name: 'Secret Hunter',
description: 'Found all hidden easter eggs',
condition: (stats) => stats.secretsFound >= 7
}],
['swarm_sympathizer', {
name: 'Swarm Sympathizer',
description: 'Showed understanding of the AI development process',
condition: (stats) => stats.metaReferencesTriggered >= 10
}]
])
}
/**
* Initialize game state directory
*/
async initialize() {
try {
await fs.mkdir(this.saveDirectory, { recursive: true })
} catch (error) {
console.warn('Could not create save directory:', error.message)
}
}
/**
* Start a new game session
* @param {string} playerName - Player's name
*/
startNewGame(playerName = 'Player') {
this.gameData.playerName = playerName
this.gameData.startTime = Date.now()
this.gameData.totalPlayTime = 0
this.gameData.achievements = new Set()
this.gameData.statistics = {
choicesMade: 0,
storiesCompleted: 0,
secretsFound: 0,
raviMockingsReceived: 0,
metaReferencesTriggered: 0
}
this.recordStatistic('choicesMade', 0) // Initialize
return this.gameData
}
/**
* Record a game statistic
* @param {string} statName - Statistic name
* @param {number} increment - Amount to increment (default 1)
*/
recordStatistic(statName, increment = 1) {
if (!(statName in this.gameData.statistics)) {
console.warn(`Unknown statistic: ${statName}`)
return
}
this.gameData.statistics[statName] += increment
this.checkAchievements()
}
/**
* Check and unlock achievements
*/
checkAchievements() {
for (const [achievementId, achievement] of this.achievementDefinitions) {
if (!this.gameData.achievements.has(achievementId)) {
if (achievement.condition(this.gameData.statistics, this.gameData)) {
this.unlockAchievement(achievementId)
}
}
}
}
/**
* Unlock an achievement
* @param {string} achievementId - Achievement identifier
*/
unlockAchievement(achievementId) {
const achievement = this.achievementDefinitions.get(achievementId)
if (!achievement) {
console.warn(`Unknown achievement: ${achievementId}`)
return
}
this.gameData.achievements.add(achievementId)
// Return achievement data for UI display
return {
id: achievementId,
name: achievement.name,
description: achievement.description,
unlockedAt: Date.now()
}
}
/**
* Get all unlocked achievements
*/
getUnlockedAchievements() {
return Array.from(this.gameData.achievements).map(id => ({
id,
...this.achievementDefinitions.get(id)
}))
}
/**
* Get achievement progress
*/
getAchievementProgress() {
const total = this.achievementDefinitions.size
const unlocked = this.gameData.achievements.size
return {
unlocked,
total,
percentage: Math.round((unlocked / total) * 100)
}
}
/**
* Update total play time
*/
updatePlayTime() {
if (this.gameData.startTime) {
this.gameData.totalPlayTime = Date.now() - this.gameData.startTime
}
}
/**
* Get formatted play time
*/
getFormattedPlayTime() {
const totalSeconds = Math.floor(this.gameData.totalPlayTime / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`
} else if (minutes > 0) {
return `${minutes}m ${seconds}s`
} else {
return `${seconds}s`
}
}
/**
* Save game state to file
* @param {string} saveName - Save file name
* @param {Object} storyState - Current story state
*/
async saveGame(saveName, storyState = {}) {
await this.initialize()
this.updatePlayTime()
const saveData = {
gameData: {
...this.gameData,
achievements: Array.from(this.gameData.achievements)
},
storyState,
savedAt: Date.now(),
version: '1.0.0'
}
const saveFile = path.join(this.saveDirectory, `${saveName}.json`)
try {
await fs.writeFile(saveFile, JSON.stringify(saveData, null, 2))
this.currentSave = saveName
return saveFile
} catch (error) {
throw new Error(`Failed to save game: ${error.message}`)
}
}
/**
* Load game state from file
* @param {string} saveName - Save file name
*/
async loadGame(saveName) {
const saveFile = path.join(this.saveDirectory, `${saveName}.json`)
try {
const saveData = JSON.parse(await fs.readFile(saveFile, 'utf8'))
this.gameData = {
...this.gameData,
...saveData.gameData,
achievements: new Set(saveData.gameData.achievements || [])
}
this.currentSave = saveName
return saveData
} catch (error) {
throw new Error(`Failed to load game: ${error.message}`)
}
}
/**
* Get list of available save files
*/
async getSaveFiles() {
await this.initialize()
try {
const files = await fs.readdir(this.saveDirectory)
const saveFiles = []
for (const file of files) {
if (file.endsWith('.json')) {
try {
const filePath = path.join(this.saveDirectory, file)
const stats = await fs.stat(filePath)
const saveData = JSON.parse(await fs.readFile(filePath, 'utf8'))
saveFiles.push({
name: file.replace('.json', ''),
playerName: saveData.gameData?.playerName || 'Unknown',
savedAt: saveData.savedAt,
playTime: this.formatTime(saveData.gameData?.totalPlayTime || 0),
size: stats.size
})
} catch (error) {
console.warn(`Corrupted save file: ${file}`)
}
}
}
return saveFiles.sort((a, b) => b.savedAt - a.savedAt)
} catch (error) {
return []
}
}
/**
* Delete a save file
* @param {string} saveName - Save file name
*/
async deleteSave(saveName) {
const saveFile = path.join(this.saveDirectory, `${saveName}.json`)
try {
await fs.unlink(saveFile)
return true
} catch (error) {
throw new Error(`Failed to delete save: ${error.message}`)
}
}
/**
* Auto-save current game state
* @param {Object} storyState - Current story state
*/
async autoSave(storyState = {}) {
if (!this.gameData.preferences.autoSave) {
return null
}
const autoSaveName = `autosave_${Date.now()}`
await this.saveGame(autoSaveName, storyState)
// Keep only the 3 most recent autosaves
await this.cleanupAutoSaves()
return autoSaveName
}
/**
* Clean up old auto-save files
*/
async cleanupAutoSaves() {
try {
const saveFiles = await this.getSaveFiles()
const autoSaves = saveFiles
.filter(save => save.name.startsWith('autosave_'))
.sort((a, b) => b.savedAt - a.savedAt)
// Delete all but the 3 most recent
for (let i = 3; i < autoSaves.length; i++) {
await this.deleteSave(autoSaves[i].name)
}
} catch (error) {
console.warn('Auto-save cleanup failed:', error.message)
}
}
/**
* Format time duration
* @param {number} milliseconds - Time in milliseconds
*/
formatTime(milliseconds) {
const totalSeconds = Math.floor(milliseconds / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return `${hours}h ${minutes}m`
} else if (minutes > 0) {
return `${minutes}m`
} else {
return `${seconds}s`
}
}
/**
* Set user preference
* @param {string} key - Preference key
* @param {*} value - Preference value
*/
setPreference(key, value) {
if (key in this.gameData.preferences) {
this.gameData.preferences[key] = value
} else {
console.warn(`Unknown preference: ${key}`)
}
}
/**
* Get user preference
* @param {string} key - Preference key
* @param {*} defaultValue - Default value
*/
getPreference(key, defaultValue = null) {
return this.gameData.preferences[key] ?? defaultValue
}
/**
* Get game statistics summary
*/
getStatsSummary() {
return {
...this.gameData.statistics,
playTime: this.getFormattedPlayTime(),
achievements: this.getAchievementProgress()
}
}
/**
* Export game data for sharing/analysis
*/
exportGameData() {
return {
playerName: this.gameData.playerName,
statistics: this.gameData.statistics,
achievements: Array.from(this.gameData.achievements),
playTime: this.gameData.totalPlayTime,
exportedAt: Date.now()
}
}
/**
* Get current save name
*/
getCurrentSave() {
return this.currentSave
}
/**
* Check if auto-save is enabled
*/
isAutoSaveEnabled() {
return this.gameData.preferences.autoSave
}
}
export default GameState