sandhill-road
Version:
A narrative-driven startup simulation game where you guide a founder from garage to exit
270 lines (243 loc) • 6.36 kB
text/typescript
// 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[];
completedExclusiveGroups: 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: 5000, // per week
users: 0,
productProgress: 0,
companyCash: 10000,
revenue: 0,
investorTrust: 5
},
stageProgress: {
currentStage: GameStage.Garage,
week: 1,
completedEvents: [],
availableEvents: [],
completedExclusiveGroups: []
},
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
}
};
});
};