Compare commits
10
Commits
0749aba648
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76523e293e | ||
|
|
9968c924b9 | ||
|
|
6ad2119b62 | ||
|
|
7b0b8ee0aa | ||
|
|
afce474e0d | ||
|
|
7352eff726 | ||
|
|
c1f578be42 | ||
|
|
d4eb926783 | ||
|
|
b962ebf214 | ||
|
|
897cb5de13 |
@@ -0,0 +1,268 @@
|
|||||||
|
# UNIFIED_API.md Update Recommendations
|
||||||
|
|
||||||
|
This document outlines the changes needed in `/vimazing/games/UNIFIED_API.md` to reflect the scoring refactor implemented in vim-maze v2.3.0.
|
||||||
|
|
||||||
|
## Summary of Changes
|
||||||
|
|
||||||
|
Vim-maze has implemented a new pattern for score management:
|
||||||
|
- **ScoreManager** is now a pure data collector (no score calculation)
|
||||||
|
- **Score calculation** moved to consumer (via utility function or custom logic)
|
||||||
|
- **Game-over conditions** are fully flexible and consumer-controlled
|
||||||
|
|
||||||
|
## Required Updates
|
||||||
|
|
||||||
|
### 1. Example Entry Point (Line ~100)
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```typescript
|
||||||
|
// vim-maze
|
||||||
|
useGame({ rows: 24, cols: 32, timeLimit: 600 })
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```typescript
|
||||||
|
// vim-maze (with custom game-over condition)
|
||||||
|
useGame({
|
||||||
|
rows: 24,
|
||||||
|
cols: 32,
|
||||||
|
gameOver: (sm) => sm.timeValue >= 600000 // 10 minutes in milliseconds
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Score Manager Type (Line ~189-201)
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```typescript
|
||||||
|
type ScoreManager = {
|
||||||
|
// Required Timing
|
||||||
|
timeValue: number; // Current time in milliseconds
|
||||||
|
|
||||||
|
// Required Metrics
|
||||||
|
totalKeystrokes: number; // Total keys pressed
|
||||||
|
|
||||||
|
// Required Result
|
||||||
|
finalScore: number | null; // 0-1000 (null until game-won)
|
||||||
|
|
||||||
|
// Optional: Game-specific metrics
|
||||||
|
// [additional fields as needed]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```typescript
|
||||||
|
type ScoreManager = {
|
||||||
|
// Required Timing
|
||||||
|
timeValue: number; // Current time in milliseconds
|
||||||
|
|
||||||
|
// Required Metrics
|
||||||
|
totalKeystrokes: number; // Total keys pressed
|
||||||
|
|
||||||
|
// Optional: Game-specific metrics
|
||||||
|
// [additional fields as needed]
|
||||||
|
// NOTE: finalScore removed - calculation moved to consumer
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add note:**
|
||||||
|
```
|
||||||
|
**Important:** ScoreManager is now a pure data collector.
|
||||||
|
Score calculation has been moved to the consumer.
|
||||||
|
Games should provide a utility function like `calculateScore()`
|
||||||
|
or instructions for score calculation in their documentation.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. GameOptions Examples (Line ~222-230)
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```typescript
|
||||||
|
// vim-maze
|
||||||
|
type GameOptions = {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
timeLimit?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```typescript
|
||||||
|
// vim-maze
|
||||||
|
type GameOptions = {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
gameOver?: boolean | ((scoreManager: ScoreManager) => boolean);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Scoring Implementation Section (Line ~257-265)
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```typescript
|
||||||
|
**vim-maze approach:**
|
||||||
|
```typescript
|
||||||
|
// Size-based: time + keystrokes × maze size
|
||||||
|
const baseScore = 1000 - timePenalty - keystrokePenalty;
|
||||||
|
const sizeMultiplier = max(1.0, (rows × cols) / 500);
|
||||||
|
const finalScore = min(1000, baseScore × sizeMultiplier);
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```typescript
|
||||||
|
**vim-maze approach (v2.3+):**
|
||||||
|
```typescript
|
||||||
|
// Pure data collection in ScoreManager
|
||||||
|
const scoreManager = useScore({ /* dependencies */ });
|
||||||
|
// scoreManager returns: { timeValue, keystrokes, optimalSteps, efficiency, ... }
|
||||||
|
|
||||||
|
// Score calculation moved to consumer
|
||||||
|
import { calculateMazeScore } from '@vimazing/vim-maze';
|
||||||
|
const finalScore = calculateMazeScore(scoreManager, mazeSize);
|
||||||
|
// Base: 1000 - (time/10) - (keystrokes/2)
|
||||||
|
// Then: multiply by size factor = max(1.0, (rows × cols) / 500)
|
||||||
|
// Final: clamp to 0-1000
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Scoring Requirements Section (Line ~203-208)
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```
|
||||||
|
**Scoring Requirements:**
|
||||||
|
- Score range: 0-1000 (capped)
|
||||||
|
- Score calculated on game-won
|
||||||
|
- Based on time and keystrokes (minimum)
|
||||||
|
- Can include game-specific penalties/bonuses
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```
|
||||||
|
**Scoring Requirements:**
|
||||||
|
- Score range: 0-1000 (capped)
|
||||||
|
- Based on time and keystrokes (minimum)
|
||||||
|
- Can include game-specific penalties/bonuses
|
||||||
|
- **NEW:** Score calculation moved to consumer via utility function
|
||||||
|
- Games should provide `calculateScore()` or similar utility
|
||||||
|
- Games should document their scoring formula clearly
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Game-Over Conditions Pattern (New Section)
|
||||||
|
|
||||||
|
Add a new subsection after "Scoring Requirements":
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Game-Over Conditions
|
||||||
|
|
||||||
|
Starting with vim-maze v2.3+, games have full control over game-over conditions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type GameOptions = {
|
||||||
|
// ...game-specific options...
|
||||||
|
gameOver?: boolean | ((scoreManager: ScoreManager) => boolean);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```typescript
|
||||||
|
// Static game-over
|
||||||
|
useGame({ rows: 24, cols: 32, gameOver: true })
|
||||||
|
|
||||||
|
// Time-based
|
||||||
|
useGame({
|
||||||
|
rows: 24,
|
||||||
|
cols: 32,
|
||||||
|
gameOver: (sm) => sm.timeValue >= 600000 // 10 minutes
|
||||||
|
})
|
||||||
|
|
||||||
|
// Efficiency-based
|
||||||
|
useGame({
|
||||||
|
rows: 24,
|
||||||
|
cols: 32,
|
||||||
|
gameOver: (sm) => sm.efficiency > 150 // >150% keystrokes
|
||||||
|
})
|
||||||
|
|
||||||
|
// Combined conditions
|
||||||
|
useGame({
|
||||||
|
rows: 24,
|
||||||
|
cols: 32,
|
||||||
|
gameOver: (sm) => sm.efficiency > 150 || sm.timeValue >= 600000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No hardcoded game-over logic in libraries
|
||||||
|
- Consumers define win/loss conditions
|
||||||
|
- Flexible for tournaments, variations, custom rules
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Hook Specifications - useScore Section (Line ~424+)
|
||||||
|
|
||||||
|
Find and update the useScore section:
|
||||||
|
|
||||||
|
**OLD:**
|
||||||
|
```typescript
|
||||||
|
export function useScore(/* dependencies */) {
|
||||||
|
// ...
|
||||||
|
const [timeValue, setTimeValue] = useState(0);
|
||||||
|
const [finalScore, setFinalScore] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Calculate score on game-won
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus === 'game-won') {
|
||||||
|
const score = calculateScore(timeValue, totalKeystrokes);
|
||||||
|
setFinalScore(Math.min(1000, Math.max(0, score)));
|
||||||
|
}
|
||||||
|
}, [gameStatus]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeValue,
|
||||||
|
totalKeystrokes,
|
||||||
|
finalScore,
|
||||||
|
// Additional metrics
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEW:**
|
||||||
|
```typescript
|
||||||
|
export function useScore(/* dependencies */) {
|
||||||
|
// Returns raw metrics only - no calculation
|
||||||
|
// Score calculation moved to consumer
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeValue,
|
||||||
|
totalKeystrokes,
|
||||||
|
// Additional game-specific metrics
|
||||||
|
// finalScore removed - consumer responsibility
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add note:
|
||||||
|
```
|
||||||
|
**Change in v2.3+:** ScoreManager is now a pure data collector.
|
||||||
|
All score calculation has been moved to the consumer.
|
||||||
|
This provides flexibility and allows games to:
|
||||||
|
- Provide utility functions for standard scoring
|
||||||
|
- Allow consumers to implement custom scoring
|
||||||
|
- Keep the hook focused on metrics collection
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Guide for Games
|
||||||
|
|
||||||
|
For games updating to this pattern:
|
||||||
|
|
||||||
|
1. Remove `finalScore` from `ScoreManager` type
|
||||||
|
2. Remove score calculation logic from `useScore` hook
|
||||||
|
3. Export a `calculateScore()` utility function
|
||||||
|
4. Update `gameOver` option to accept function receiving `scoreManager`
|
||||||
|
5. Remove `timeLimit` logic from `useScore` (delegate to consumer)
|
||||||
|
6. Document the new scoring approach in gameInfo
|
||||||
|
|
||||||
|
## Benefits of This Pattern
|
||||||
|
|
||||||
|
✅ **Pure Data Collection** - ScoreManager has no side effects
|
||||||
|
✅ **Separation of Concerns** - Scoring logic is independent
|
||||||
|
✅ **Flexibility** - Consumers control both scoring and game-over
|
||||||
|
✅ **Testability** - Scoring functions can be tested in isolation
|
||||||
|
✅ **Reusability** - Metrics available for any custom logic
|
||||||
|
✅ **Clarity** - Clear boundaries between collection and calculation
|
||||||
+42
-37
@@ -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>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@vimazing/vim-maze",
|
"name": "@vimazing/vim-maze",
|
||||||
"version": "2.2.0",
|
"version": "2.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@vimazing/vim-maze",
|
"name": "@vimazing/vim-maze",
|
||||||
"version": "2.2.0",
|
"version": "2.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vimazing/vim-maze",
|
"name": "@vimazing/vim-maze",
|
||||||
"version": "2.2.0",
|
"version": "2.3.1",
|
||||||
"description": "VIMazing VIM Maze engine — a lightweight, typed React hook set for maze-based games.",
|
"description": "VIMazing VIM Maze engine — a lightweight, typed React hook set for maze-based games.",
|
||||||
"author": "André Padez <you@example.com>",
|
"author": "André Padez <you@example.com>",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -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 };
|
||||||
+83
-61
@@ -1,4 +1,7 @@
|
|||||||
|
import { version } from '../package.json';
|
||||||
|
|
||||||
export const gameInfo = {
|
export const gameInfo = {
|
||||||
|
version,
|
||||||
name: 'VIM Maze',
|
name: 'VIM Maze',
|
||||||
description: 'Navigate procedurally generated mazes with VIM-style controls',
|
description: 'Navigate procedurally generated mazes with VIM-style controls',
|
||||||
|
|
||||||
@@ -57,56 +60,72 @@ export const gameInfo = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
scoring: {
|
scoring: {
|
||||||
formula: 'Base Score = 1000 - (time penalty) - (keystroke penalty)',
|
calculation: 'Call calculateMazeScore(scoreManager, mazeSize) to compute final score',
|
||||||
multiplier: 'Final Score = min(1000, Base Score × size multiplier)',
|
note: 'Score calculation moved to consumer - ScoreManager only provides raw metrics',
|
||||||
range: '0 - 1000 points',
|
formula: 'Base Score = 1000 - (time penalty) - (keystroke penalty)',
|
||||||
|
multiplier: 'Final Score = min(1000, Base Score × size multiplier)',
|
||||||
penalties: [
|
range: '0 - 1000 points',
|
||||||
{ factor: 'Time', formula: 'seconds / 10', example: '60 seconds = -6 points' },
|
|
||||||
{ factor: 'Keystrokes', formula: 'total keys / 2', example: '100 keys = -50 points' },
|
penalties: [
|
||||||
],
|
{ factor: 'Time', formula: 'seconds / 10', example: '60 seconds = -6 points' },
|
||||||
|
{ factor: 'Keystrokes', formula: 'total keys / 2', example: '100 keys = -50 points' },
|
||||||
sizeMultiplier: {
|
],
|
||||||
formula: 'max(1.0, (rows × cols) / 500)',
|
|
||||||
description: 'Larger mazes earn higher potential scores',
|
sizeMultiplier: {
|
||||||
examples: [
|
formula: 'max(1.0, (rows × cols) / 500)',
|
||||||
{ size: '16×24 (384 cells)', multiplier: '1.0x', description: 'Small maze, no bonus' },
|
description: 'Larger mazes earn higher potential scores',
|
||||||
{ size: '24×32 (768 cells)', multiplier: '1.54x', description: 'Medium maze' },
|
examples: [
|
||||||
{ size: '32×48 (1536 cells)', multiplier: '3.07x', description: 'Large maze' },
|
{ size: '16×24 (384 cells)', multiplier: '1.0x', description: 'Small maze, no bonus' },
|
||||||
],
|
{ size: '24×32 (768 cells)', multiplier: '1.54x', description: 'Medium maze' },
|
||||||
},
|
{ size: '32×48 (1536 cells)', multiplier: '3.07x', description: 'Large maze' },
|
||||||
|
],
|
||||||
examples: [
|
},
|
||||||
{
|
|
||||||
size: '16×24',
|
examples: [
|
||||||
time: '60 seconds',
|
{
|
||||||
keystrokes: 80,
|
size: '16×24',
|
||||||
calculation: '1000 - 6 - 40 = 954 × 1.0',
|
time: '60 seconds',
|
||||||
score: '954 / 1000',
|
keystrokes: 80,
|
||||||
},
|
calculation: '1000 - 6 - 40 = 954 × 1.0',
|
||||||
{
|
score: '954 / 1000',
|
||||||
size: '32×48',
|
},
|
||||||
time: '180 seconds',
|
{
|
||||||
keystrokes: 200,
|
size: '32×48',
|
||||||
calculation: '1000 - 18 - 100 = 882 × 3.07',
|
time: '180 seconds',
|
||||||
score: '1000 / 1000 (capped)',
|
keystrokes: 200,
|
||||||
},
|
calculation: '1000 - 18 - 100 = 882 × 3.07',
|
||||||
],
|
score: '1000 / 1000 (capped)',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
gameOver: {
|
gameOver: {
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
type: 'Time Limit',
|
type: 'Custom Condition',
|
||||||
trigger: '10 minutes (600 seconds) by default',
|
trigger: 'Defined via gameOver option in GameOptions',
|
||||||
configurable: true,
|
configurable: true,
|
||||||
message: 'Time ran out!',
|
message: 'Game over!',
|
||||||
description: 'Failed to reach exit with key in time',
|
description: 'Can be a boolean or function that receives scoreManager metrics',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
note: 'No other game-over conditions - take your time and find the optimal path!',
|
examples: [
|
||||||
},
|
{
|
||||||
|
description: 'Time-based (10 minutes)',
|
||||||
|
code: 'gameOver: (sm) => sm.timeValue >= 600000',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Efficiency-based (exceed 150%)',
|
||||||
|
code: 'gameOver: (sm) => sm.efficiency > 150',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Combined condition',
|
||||||
|
code: 'gameOver: (sm) => sm.efficiency > 150 || sm.timeValue >= 600000',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
note: 'Game-over conditions are flexible and controlled by the game consumer. No built-in limits - define your own rules!',
|
||||||
|
},
|
||||||
|
|
||||||
mazeGeneration: {
|
mazeGeneration: {
|
||||||
algorithm: 'Depth-first search with backtracking',
|
algorithm: 'Depth-first search with backtracking',
|
||||||
@@ -123,19 +142,22 @@ export const gameInfo = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
metrics: {
|
metrics: {
|
||||||
tracked: [
|
tracked: [
|
||||||
{ metric: 'Time', unit: 'seconds', description: 'Elapsed time since game start' },
|
{ metric: 'Time', unit: 'milliseconds', description: 'Elapsed time since game start' },
|
||||||
{ metric: 'Keystrokes', unit: 'count', description: 'Total keys pressed' },
|
{ metric: 'Keystrokes', unit: 'count', description: 'Total keys pressed' },
|
||||||
{ metric: 'Distance to Key', unit: 'cells', description: 'Current shortest path to key' },
|
{ metric: 'Optimal Steps', unit: 'cells', description: 'Perfect path length (entrance → key → exit)' },
|
||||||
{ metric: 'Distance to Exit', unit: 'cells', description: 'Current shortest path to exit' },
|
{ metric: 'Efficiency', unit: 'percentage', description: 'Keystroke efficiency ratio (100% = perfect)' },
|
||||||
],
|
{ metric: 'Distance to Key', unit: 'cells', description: 'Current shortest path to key' },
|
||||||
displayed: 'Live during gameplay to help you track progress',
|
{ metric: 'Distance to Exit', unit: 'cells', description: 'Current shortest path to exit' },
|
||||||
},
|
],
|
||||||
|
available: 'All metrics available via scoreManager for use in scoring or game-over conditions',
|
||||||
|
displayed: 'Live during gameplay to help you track progress and make decisions',
|
||||||
|
},
|
||||||
|
|
||||||
objective: 'Navigate from entrance to key, then from key to exit, before time runs out.',
|
objective: 'Navigate from entrance to key, then from key to exit with a custom game-over condition.',
|
||||||
|
|
||||||
winCondition: 'Reach the exit cell while holding the key (key must be collected first).',
|
winCondition: 'Reach the exit cell while holding the key (key must be collected first).',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type GameInfo = typeof gameInfo;
|
export type GameInfo = typeof gameInfo;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ export function useScore({ board, hero, gameStatusManager, keyManager, gameOver
|
|||||||
}, [gameStatus, board.mazeInstanceRef]);
|
}, [gameStatus, board.mazeInstanceRef]);
|
||||||
|
|
||||||
const optimalSteps = optimalRef.current;
|
const optimalSteps = optimalRef.current;
|
||||||
const efficiency = optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0;
|
// Efficiency: 100% = optimal play, 0% = worst case
|
||||||
|
// Formula: (optimalSteps / keystrokes) * 100, clamped to 0-100
|
||||||
|
const efficiency = optimalSteps > 0 && keystrokes > 0
|
||||||
|
? Math.max(0, Math.min(100, Math.round((optimalSteps / keystrokes) * 100)))
|
||||||
|
: 100;
|
||||||
|
|
||||||
// Check custom gameOver condition if provided
|
// Check custom gameOver condition if provided
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -54,7 +54,5 @@ export function useTimer(): TimerManager {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
console.log('timeValue', timeValue);
|
|
||||||
|
|
||||||
return { timeValue, startTimer, stopTimer, resetTimer };
|
return { timeValue, startTimer, stopTimer, resetTimer };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user