feat: add runtime gameInfo enrichment with gameOver condition

- Add gameOverCondition to GameManager for runtime tracking
- Create enrichGameInfo() function to merge runtime condition with static gameInfo
- Add formatGameOverCondition() utility to stringify boolean/function conditions
- Returns enriched info with:
  - condition: formatted string representation
  - type: 'none' | 'static' | 'function'
- Platform can call enrichGameInfo(gameManager) to get complete game configuration
- Handles all condition types: undefined, boolean, function
- Truncates long function strings at 200 chars
This commit is contained in:
2025-11-02 17:20:21 +00:00
parent afce474e0d
commit 7b0b8ee0aa
5 changed files with 99 additions and 37 deletions
+42 -37
View File
@@ -1,4 +1,4 @@
import { useGame, version, calculateMazeScore } from "../../src"; import { useGame, version, calculateMazeScore, type ScoreManager } from "../../src";
import { useKeyBindings } from "./useKeyBindings"; import { useKeyBindings } from "./useKeyBindings";
import { useIsMobile } from "./useIsMobile"; import { useIsMobile } from "./useIsMobile";
import { useMemo } from "react"; import { useMemo } from "react";
@@ -9,7 +9,12 @@ function App() {
const cols = isMobile ? 12 : 32; const cols = isMobile ? 12 : 32;
const rows = isMobile ? 8 : 24; const rows = isMobile ? 8 : 24;
const gameManager = useGame({ cols, rows }, useKeyBindings); const gameOver = (scroreManager: ScoreManager) => {
console.log('gameOver', scroreManager);
return scoreManager.efficiency < 70;
}
const gameManager = useGame({ cols, rows, gameOver }, useKeyBindings);
const { containerRef, gameStatus, scoreManager } = gameManager; const { containerRef, gameStatus, scoreManager } = gameManager;
const { timeValue, keystrokes, optimalSteps, efficiency } = scoreManager; const { timeValue, keystrokes, optimalSteps, efficiency } = scoreManager;
@@ -30,41 +35,41 @@ function App() {
<div className="flex flex-col items-center justify-around min-h-[100vh]"> <div className="flex flex-col items-center justify-around min-h-[100vh]">
<h1 className="text-6xl">VIMazing vim-maze <small>v{version}</small> example</h1> <h1 className="text-6xl">VIMazing vim-maze <small>v{version}</small> example</h1>
<div className="mx-auto my-4 w-fit bg-muted rounded-lg p-4"> <div className="mx-auto my-4 w-fit bg-muted rounded-lg p-4">
{(gameStatus === "started" || gameStatus === "has-key" || gameStatus === "game-won") && ( {(gameStatus === "started" || gameStatus === "has-key" || gameStatus === "game-won") && (
<div className="flex gap-8 text-sm font-mono items-center"> <div className="flex gap-8 text-sm font-mono items-center">
<div> <div>
<div className="text-muted-foreground">Time</div> <div className="text-muted-foreground">Time</div>
<div className="text-lg font-bold">{formatTime(timeValue)}</div> <div className="text-lg font-bold">{formatTime(timeValue)}</div>
</div> </div>
<div> <div>
<div className="text-muted-foreground">Keystrokes</div> <div className="text-muted-foreground">Keystrokes</div>
<div className="text-lg font-bold">{keystrokes}</div> <div className="text-lg font-bold">{keystrokes}</div>
</div> </div>
{optimalSteps > 0 && ( {optimalSteps > 0 && (
<> <>
<div className="w-px h-12 bg-muted-foreground/20" /> <div className="w-px h-12 bg-muted-foreground/20" />
<div> <div>
<div className="text-muted-foreground">Optimal Steps</div> <div className="text-muted-foreground">Optimal Steps</div>
<div className="text-lg font-bold">{optimalSteps}</div> <div className="text-lg font-bold">{optimalSteps}</div>
</div> </div>
<div> <div>
<div className="text-muted-foreground">Efficiency</div> <div className="text-muted-foreground">Efficiency</div>
<div className="text-lg font-bold">{efficiency}%</div> <div className="text-lg font-bold">{efficiency}%</div>
</div> </div>
</> </>
)} )}
{gameStatus === "game-won" && finalScore && ( {gameStatus === "game-won" && finalScore && (
<> <>
<div className="w-px h-12 bg-muted-foreground/20" /> <div className="w-px h-12 bg-muted-foreground/20" />
<div className="text-center"> <div className="text-center">
<div className="text-muted-foreground">Final Score</div> <div className="text-muted-foreground">Final Score</div>
<div className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">{finalScore}</div> <div className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-500 bg-clip-text text-transparent">{finalScore}</div>
</div> </div>
</> </>
)} )}
</div> </div>
)} )}
</div> </div>
<div className="relative mx-auto my-4 w-fit" id="maze_container"> <div className="relative mx-auto my-4 w-fit" id="maze_container">
<div ref={containerRef} className="relative" /> <div ref={containerRef} className="relative" />
</div> </div>
+54
View File
@@ -0,0 +1,54 @@
import type { GameManager } from './types';
import { gameInfo as baseGameInfo } from './gameInfo';
/**
* Format the gameOver condition as a readable string
*/
function formatGameOverCondition(condition: GameManager['gameOverCondition']): string {
if (condition === undefined) {
return 'None - game continues indefinitely';
}
if (condition === true) {
return 'Always (immediate game over)';
}
if (condition === false) {
return 'Never (game never ends)';
}
if (typeof condition === 'function') {
// Try to get the function source
const source = condition.toString();
// Clean up and format
const cleaned = source
.replace(/\s+/g, ' ')
.replace(/=>/, '→')
.trim();
return cleaned.length > 200 ? cleaned.substring(0, 197) + '...' : cleaned;
}
return 'Unknown condition';
}
/**
* Enrich gameInfo with runtime gameOver condition
* Returns the base gameInfo plus runtime condition info
*/
export function enrichGameInfo(gameManager: GameManager) {
const runtimeCondition = formatGameOverCondition(gameManager.gameOverCondition);
return {
...baseGameInfo,
gameOverRuntime: {
condition: runtimeCondition,
type: gameManager.gameOverCondition === undefined
? 'none'
: typeof gameManager.gameOverCondition === 'boolean'
? 'static'
: 'function',
},
};
}
export { formatGameOverCondition };
+1
View File
@@ -3,6 +3,7 @@ export * from './useGame';
export * from './useGameStatus'; export * from './useGameStatus';
export { gameInfo } from './gameInfo'; export { gameInfo } from './gameInfo';
export { calculateMazeScore } from './useScore'; export { calculateMazeScore } from './useScore';
export { enrichGameInfo, formatGameOverCondition } from './formatGameInfo';
import { version } from '../package.json'; import { version } from '../package.json';
export type { GameInfo } from './gameInfo'; export type { GameInfo } from './gameInfo';
export { version }; export { version };
+1
View File
@@ -29,4 +29,5 @@ export type GameManager = {
hero: HeroManager; hero: HeroManager;
renderer: HeroRenderManager; renderer: HeroRenderManager;
scoreManager: ScoreManager; scoreManager: ScoreManager;
gameOverCondition?: boolean | ((scoreManager: ScoreManager) => boolean);
} & GameStatusManager & GameKeyManager; } & GameStatusManager & GameKeyManager;
+1
View File
@@ -25,6 +25,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
hero, hero,
renderer, renderer,
scoreManager, scoreManager,
gameOverCondition: gameOver,
} }
if (typeof platformHook === 'function') { if (typeof platformHook === 'function') {