UNPKG

sandhill-road

Version:

A narrative-driven startup simulation game where you guide a founder from garage to exit

268 lines (241 loc) 6.28 kB
// Game state manager for Sandhill Road // Tracks all mutable game values import { saveToFile, loadFromFile } from '../utils/fileStorage'; export type FounderStats = { health: number; morale: number; hustle: number; tech: number; luck: number; stamina: number; maxStamina: number; staminaRegen: number; personalCash: number; }; export type CompanyStats = { runway: number; burnRate: number; users: number; productProgress: number; companyCash: number; revenue: number; investorTrust: number; }; export enum GameStage { Garage = "Garage", DemoDay = "Demo Day", Fundraising = "Fundraising", PMF = "PMF", Scaling = "Scaling", Crisis = "Crisis", Exit = "Exit" } export type StageProgress = { currentStage: GameStage; week: number; completedEvents: string[]; availableEvents: string[]; }; export type CompanyFlags = { hasCoFounder: boolean; coFounderEquity: number; inAccelerator: boolean; acceleratorName?: string; hasRaisedSeed: boolean; hasRaisedSeriesA: boolean; hasPMF: boolean; hasFirstEmployee: boolean; hasOffice: boolean; hasBoard: boolean; majorCrisisSurvived: boolean; exitOffersReceived: number; }; export type GameState = { founderName: string; companyName: string; founderStats: FounderStats; companyStats: CompanyStats; stageProgress: StageProgress; companyFlags: CompanyFlags; gameOver: boolean; gameOverReason?: string; }; // Default starting values for a new game export const createInitialGameState = ( founderName: string, companyName: string, initialPersonalCash: number = 30000 ): GameState => { return { founderName, companyName, founderStats: { health: 8, morale: 10, hustle: 5, tech: 5, luck: 5, stamina: 10, maxStamina: 10, staminaRegen: 3, personalCash: initialPersonalCash }, companyStats: { runway: 12, // weeks burnRate: 2000, // per week users: 0, productProgress: 0, companyCash: 10000, revenue: 0, investorTrust: 5 }, stageProgress: { currentStage: GameStage.Garage, week: 1, completedEvents: [], availableEvents: [] }, companyFlags: { hasCoFounder: false, coFounderEquity: 0, inAccelerator: false, acceleratorName: undefined, hasRaisedSeed: false, hasRaisedSeriesA: false, hasPMF: false, hasFirstEmployee: false, hasOffice: false, hasBoard: false, majorCrisisSurvived: false, exitOffersReceived: 0 }, gameOver: false }; }; // Game state singleton let gameState: GameState | null = null; // Game state methods export const initGame = (founderName: string, companyName: string, initialCash: number = 30000): GameState => { gameState = createInitialGameState(founderName, companyName, initialCash); return gameState; }; export const getGameState = (): GameState => { if (!gameState) { throw new Error("Game state not initialized. Call initGame first."); } return gameState; }; export const updateGameState = (updater: (state: GameState) => GameState): GameState => { if (!gameState) { throw new Error("Game state not initialized. Call initGame first."); } gameState = updater(gameState); return gameState; }; export const saveGame = (): void => { if (!gameState) { throw new Error("No game to save"); } try { // Check if we're in a browser or Node environment if (typeof window !== 'undefined' && window.localStorage) { localStorage.setItem('sandhillRoadSave', JSON.stringify(gameState)); } else { // In Node.js, use file storage saveToFile(gameState); } } catch (error) { console.error("Failed to save game:", error); } }; export const loadGame = (): GameState | null => { try { let savedGame: string | null = null; // Check if we're in a browser or Node environment if (typeof window !== 'undefined' && window.localStorage) { savedGame = localStorage.getItem('sandhillRoadSave'); if (savedGame) { gameState = JSON.parse(savedGame); return gameState; } } else { // In Node.js, use file storage const data = loadFromFile(); if (data) { gameState = data; return gameState; } } } catch (error) { console.error("Failed to load game:", error); } return null; }; export const endGame = (reason: string): void => { if (!gameState) return; updateGameState(state => ({ ...state, gameOver: true, gameOverReason: reason })); }; // Weekly update function export const advanceWeek = (): void => { if (!gameState || gameState.gameOver) return; updateGameState(state => { // Update week counter const newWeek = state.stageProgress.week + 1; // Calculate new values const newStamina = Math.min( state.founderStats.maxStamina, state.founderStats.stamina + state.founderStats.staminaRegen ); const newCompanyCash = state.companyStats.companyCash - state.companyStats.burnRate; const newRunway = Math.floor(newCompanyCash / state.companyStats.burnRate); // Check for game over conditions if (newCompanyCash <= 0) { return { ...state, companyStats: { ...state.companyStats, companyCash: 0, runway: 0 }, stageProgress: { ...state.stageProgress, week: newWeek }, gameOver: true, gameOverReason: "Your company ran out of money." }; } if (state.founderStats.health <= 0) { return { ...state, stageProgress: { ...state.stageProgress, week: newWeek }, gameOver: true, gameOverReason: "Your health deteriorated completely. You had to step down." }; } // Normal weekly update return { ...state, founderStats: { ...state.founderStats, stamina: newStamina }, companyStats: { ...state.companyStats, companyCash: newCompanyCash, runway: newRunway }, stageProgress: { ...state.stageProgress, week: newWeek } }; }); };