Compare commits
10
Commits
| 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
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
import { useGame, version, calculateMazeScore } from "../../src";
|
||||
import { useGame, version, calculateMazeScore, type ScoreManager } from "../../src";
|
||||
import { useKeyBindings } from "./useKeyBindings";
|
||||
import { useIsMobile } from "./useIsMobile";
|
||||
import { useMemo } from "react";
|
||||
@@ -9,7 +9,12 @@ function App() {
|
||||
const cols = isMobile ? 12 : 32;
|
||||
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 { timeValue, keystrokes, optimalSteps, efficiency } = scoreManager;
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vimazing/vim-maze",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vimazing/vim-maze",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.3"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"author": "André Padez <you@example.com>",
|
||||
"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 };
|
||||
+30
-8
@@ -1,4 +1,7 @@
|
||||
import { version } from '../package.json';
|
||||
|
||||
export const gameInfo = {
|
||||
version,
|
||||
name: 'VIM Maze',
|
||||
description: 'Navigate procedurally generated mazes with VIM-style controls',
|
||||
|
||||
@@ -58,6 +61,8 @@ export const gameInfo = {
|
||||
},
|
||||
|
||||
scoring: {
|
||||
calculation: 'Call calculateMazeScore(scoreManager, mazeSize) to compute final score',
|
||||
note: 'Score calculation moved to consumer - ScoreManager only provides raw metrics',
|
||||
formula: 'Base Score = 1000 - (time penalty) - (keystroke penalty)',
|
||||
multiplier: 'Final Score = min(1000, Base Score × size multiplier)',
|
||||
range: '0 - 1000 points',
|
||||
@@ -98,14 +103,28 @@ export const gameInfo = {
|
||||
gameOver: {
|
||||
conditions: [
|
||||
{
|
||||
type: 'Time Limit',
|
||||
trigger: '10 minutes (600 seconds) by default',
|
||||
type: 'Custom Condition',
|
||||
trigger: 'Defined via gameOver option in GameOptions',
|
||||
configurable: true,
|
||||
message: 'Time ran out!',
|
||||
description: 'Failed to reach exit with key in time',
|
||||
message: 'Game over!',
|
||||
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: {
|
||||
@@ -125,15 +144,18 @@ export const gameInfo = {
|
||||
|
||||
metrics: {
|
||||
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: 'Optimal Steps', unit: 'cells', description: 'Perfect path length (entrance → key → exit)' },
|
||||
{ metric: 'Efficiency', unit: 'percentage', description: 'Keystroke efficiency ratio (100% = perfect)' },
|
||||
{ metric: 'Distance to Key', unit: 'cells', description: 'Current shortest path to key' },
|
||||
{ metric: 'Distance to Exit', unit: 'cells', description: 'Current shortest path to exit' },
|
||||
],
|
||||
displayed: 'Live during gameplay to help you track progress',
|
||||
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).',
|
||||
} as const;
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './useGame';
|
||||
export * from './useGameStatus';
|
||||
export { gameInfo } from './gameInfo';
|
||||
export { calculateMazeScore } from './useScore';
|
||||
export { enrichGameInfo, formatGameOverCondition } from './formatGameInfo';
|
||||
import { version } from '../package.json';
|
||||
export type { GameInfo } from './gameInfo';
|
||||
export { version };
|
||||
|
||||
@@ -29,4 +29,5 @@ export type GameManager = {
|
||||
hero: HeroManager;
|
||||
renderer: HeroRenderManager;
|
||||
scoreManager: ScoreManager;
|
||||
gameOverCondition?: boolean | ((scoreManager: ScoreManager) => boolean);
|
||||
} & GameStatusManager & GameKeyManager;
|
||||
|
||||
@@ -25,6 +25,7 @@ export function useGame(options: GameOptions, platformHook?: unknown): GameManag
|
||||
hero,
|
||||
renderer,
|
||||
scoreManager,
|
||||
gameOverCondition: gameOver,
|
||||
}
|
||||
|
||||
if (typeof platformHook === 'function') {
|
||||
|
||||
@@ -50,7 +50,11 @@ export function useScore({ board, hero, gameStatusManager, keyManager, gameOver
|
||||
}, [gameStatus, board.mazeInstanceRef]);
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
|
||||
@@ -54,7 +54,5 @@ export function useTimer(): TimerManager {
|
||||
};
|
||||
}, []);
|
||||
|
||||
console.log('timeValue', timeValue);
|
||||
|
||||
return { timeValue, startTimer, stopTimer, resetTimer };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user