npm package: @hrejko/core
Description: Core reusable utilities and base classes for Hrejko games
Latest version: 1.0.0

# @hrejko/core - Reusable Game Framework Utilities

Provides common types, utilities, and patterns for building Hrejko games. Designed to reduce boilerplate when creating game examples.

## What's Included

### Types (`@hrejko/core/types`)

- **Movement:** Direction vectors, movement context, helpers
- **Collision:** Collision results, events, types, grid collision detection
- **Events:** Tick context, tick metrics, game results, AI bot context, game snapshots

### Utilities (`@hrejko/core/utils`)

- **Map Generation:** `createBorderedMap()`, `validateMapConnectivity()`, `placeRandomObstacles()`, map analysis
- **Spawn Logic:** `findRandomSpawnPosition()`, `findNearestSpawnPosition()`, `findEmptyCell()`, spawn validation
- **Pathfinding:** `findPath()` (BFS), `getDirectionToTarget()`, `findNearestOnGrid()`, `manhattanDistance()`
- **Collision:** `checkGridCollision()`, `checkCollectible()`, `buildOccupiedSet()`, `collidesWithSelfSegments()`

### Server Utilities (`@hrejko/core/server`)

- **Game Loop:** `GameLoopFactory.create()`, tick metrics, pause/resume
- **Game Lifecycle:** `handlePauseResume()`, `handleStartCountdown()` — generic pause/resume/start logic
- **Bot AI:** `createStrategyBot()`, `IBotAI` type — strategy-based bot framework with pluggable behaviors

## Type Convention

All types in `@hrejko/core` use **`type` aliases** (not `interface`). This is a project-wide convention. See [DEVELOPER.md](../../DEVELOPER.md#type-aliases-over-interfaces) for details.

## Quick Start

### 1. Map Generation

```typescript
import { createBorderedMap, validateMapConnectivity } from "@hrejko/core";

// Create a 20x20 bordered map
const map = createBorderedMap(20);

// Validate all open spaces are connected
if (!validateMapConnectivity(map)) {
  throw new Error("Map has unreachable islands!");
}
```

### 2. Spawn Positions

```typescript
import { findRandomSpawnPosition, findFirstSpawnPosition } from "@hrejko/core";

const occupiedPositions = [
  { x: 5, y: 5 },
  { x: 10, y: 10 },
];

// Random spawn (good for multiplayer)
const spawnPos = findRandomSpawnPosition(map, occupiedPositions);

// Sequential spawn (deterministic, for testing)
const testSpawn = findFirstSpawnPosition(map, occupiedPositions);
```

### 3. Game Loop

```typescript
import { GameLoopFactory } from "@hrejko/core";

const loop = GameLoopFactory.create({ tickRateHz: 60 });

loop.registerHandler({
  onTick: (context) => {
    console.log(`Tick ${context.tick} - FPS: ${context.fps.toFixed(1)}`);
    // Update game state
  },
});

loop.start();
// ... later
loop.stop();
```

### 4. Direction Vectors & Movement

```typescript
import {
  DIRECTION_VECTORS,
  getDirectionVector,
  isOppositeDirection,
} from "@hrejko/core";

// Use predefined vectors
const moveUp = DIRECTION_VECTORS["UP"]; // { x: 0, y: -1 }

// Calculate next position
const nextPos = {
  x: player.x + moveUp.x,
  y: player.y + moveUp.y,
};

// Prevent 180° reverse moves
if (isOppositeDirection("UP", "DOWN")) {
  console.log("Prevented self-kill!");
}
```

### 5. Pathfinding (BFS)

```typescript
import {
  findPath,
  getDirectionToTarget,
  manhattanDistance,
} from "@hrejko/core";

// Find shortest path from A to B on the map
const path = findPath(map, { x: 1, y: 1 }, { x: 10, y: 10 });
if (path.length > 0) {
  const nextStep = path[0]; // First step toward target
}

// Get the direction to move toward a target
const direction = getDirectionToTarget({ x: 1, y: 1 }, { x: 3, y: 1 });
// Returns 'RIGHT'

// Calculate manhattan distance between two points
const dist = manhattanDistance({ x: 1, y: 1 }, { x: 4, y: 5 });
// Returns 7
```

### 6. Grid Collision Detection

```typescript
import {
  checkGridCollision,
  checkCollectible,
  buildOccupiedSet,
} from "@hrejko/core";

// Build a set of occupied positions from multiple entity groups
const occupied = buildOccupiedSet([selfSegments, otherSegments]);

// Check if a position collides with walls, boundaries, or entities
const collision = checkGridCollision(newHead, map, occupied);
if (collision.type !== "none") {
  console.log(`Collision: ${collision.type}`);
}

// Check if a position has a collectible (bonus tile)
const collectible = checkCollectible(newHead, map);
if (collectible.type === "collectible") {
  player.score += 1;
}
```

### 7. Bot AI with Strategy Pattern

```typescript
import { createStrategyBot } from "@hrejko/core";
import type { BotStrategy, StrategyBotConfig } from "@hrejko/core";

// Define game-specific strategies
const chaseFood: BotStrategy<MyPlayer, MyGame> = {
  name: "chaseFood",
  priority: 1,
  decide: (context) => {
    const food = findNearestFood(context.game);
    if (!food) return null;
    return getDirectionToTarget(context.headPosition, food);
  },
};

// Create a bot with pluggable strategies
const bot = createStrategyBot<MyPlayer, MyGame>({
  getHeadPosition: (p) => p.segments[0],
  getMap: (g) => g.map,
  getOccupiedPositions: (g) => buildOccupiedSet(g),
  getCurrentDirection: (p) => p.direction,
  hasMultipleSegments: (p) => p.segments.length > 1,
  strategies: [chaseFood, avoidDanger, wander],
});

// Use it in your game loop
const direction = bot.decideAction(player, game);
```

### 8. Game Lifecycle (Pause/Resume/Start Countdown)

```typescript
import { handlePauseResume, handleStartCountdown } from "@hrejko/core";

// Generic pause/resume with countdown
const result = handlePauseResume(game, {
  resumeCountdownMs: 5000,
  onPause: (g) => console.log("Paused"),
  onResume: (g) => console.log("Resumed"),
  onResumeCountdownStart: (g) => console.log("Countdown started"),
});

if (result.changed) {
  broadcastGameState(game);
}
```

### 9. Random Obstacle Placement

```typescript
import { createBorderedMap, placeRandomObstacles } from "@hrejko/core";

const map = createBorderedMap(20);
const obstacleMap = placeRandomObstacles(map, {
  targetCount: 15,
  ensureConnectivity: true, // Prevents unreachable islands
  minOpenSpaces: 10,
});
```

## Common Patterns

### Pattern 1: Map Setup

```typescript
import {
  createBorderedMap,
  validateMapConnectivity,
  countOpenSpaces,
  findRandomSpawnPosition,
} from "@hrejko/core";

function setupGameMap(playerCount: number) {
  const mapSize = 8 + playerCount * 2;
  const map = createBorderedMap(mapSize);

  if (!validateMapConnectivity(map)) {
    throw new Error("Generated map is invalid");
  }

  console.log(
    `Created ${mapSize}x${mapSize} map with ${countOpenSpaces(map)} open spaces`,
  );
  return map;
}
```

### Pattern 2: Player Spawning

```typescript
import { findRandomSpawnPosition, findAllSpawnPositions } from "@hrejko/core";

class GameRoom {
  private players: Player[] = [];

  addPlayer(player: Player): boolean {
    const occupiedPositions = this.players.map((p) => ({ x: p.x, y: p.y }));
    const spawn = findRandomSpawnPosition(this.map, occupiedPositions);

    if (!spawn) {
      console.log("Room full!");
      return false;
    }

    player.x = spawn.x;
    player.y = spawn.y;
    this.players.push(player);
    return true;
  }

  getCapacity(): number {
    const occupiedPositions = this.players.map((p) => ({ x: p.x, y: p.y }));
    const available = findAllSpawnPositions(this.map, occupiedPositions);
    return available.length;
  }
}
```

### Pattern 3: Game Tick Loop

```typescript
import { GameLoopFactory } from "@hrejko/core";

class SnakeGameServer {
  private loop = GameLoopFactory.create({ tickRateHz: 60 });

  initialize() {
    this.loop.registerHandler({
      onTick: (context) => this.handleTick(context),
      onGameStart: (gameId) => this.handleGameStart(gameId),
      onGameEnd: (gameId) => this.handleGameEnd(gameId),
    });

    this.loop.start();
  }

  private handleTick(context: TickContext) {
    // Update all games in this tick
    for (const game of this.getActiveGames()) {
      this.updateGameState(game, context);
      this.checkCollisions(game);
      this.broadcastGameState(game);
    }
  }

  shutdown() {
    this.loop.stop();
  }
}
```

## API Reference

### Map Generation

#### `createBorderedMap(size, options?)`

- Creates a square bordered map
- `size`: Width and height
- Returns: GameMap

#### `validateMapConnectivity(map, openChar?)`

- Checks all open spaces form one connected region (BFS)
- Returns: boolean

#### `countOpenSpaces(map, openChar?)`

- Returns: number

#### `getAllOpenPositions(map, openChar?)`

- Returns: Position[]

#### `placeRandomObstacles(map, options)`

- Places random wall tiles while ensuring map connectivity
- Options: `targetCount`, `ensureConnectivity`, `minOpenSpaces`, `maxAttemptsPerObstacle`
- Returns: GameMap (new map with obstacles)

### Spawn Logic

#### `findRandomSpawnPosition(map, occupied?, openChar?, maxAttempts?)`

- Random spawn (up to maxAttempts)
- Returns: Position | null

#### `findFirstSpawnPosition(map, occupied?, openChar?)`

- Sequential spawn (deterministic)
- Returns: Position | null

#### `findNearestSpawnPosition(map, occupied?, preferredX?, preferredY?, openChar?)`

- Spawn nearest to preferred location
- Returns: Position | null

#### `findAllSpawnPositions(map, occupied?, openChar?)`

- Get all available positions
- Returns: Position[]

#### `findEmptyCell(map, occupiedSet?, openChar?)`

- Find any empty cell not in the occupied set
- Returns: `{ x, y }` | null

#### `isValidSpawnPosition(position, map, occupied?, openChar?)`

- Validate a specific position
- Returns: boolean

#### `countAvailableSpawns(map, occupied?, openChar?)`

- Returns: number

### Pathfinding

#### `findPath(map, start, target, options?)`

- BFS pathfinding from start to target on a grid map
- Options: `isWalkable`, `wallChars`, `maxIterations`
- Returns: GridPosition[] (path excluding start, including target)

#### `getDirectionToTarget(from, to)`

- Returns the cardinal direction from one position toward another
- Returns: PlayerDirection | null

#### `findNearestOnGrid(start, candidates)`

- Find the nearest position from a list using manhattan distance
- Returns: GridPosition | null

#### `manhattanDistance(a, b)`

- Returns: number

#### `getNextPosition(position, direction)`

- Apply a direction to get the next grid position
- Returns: GridPosition

### Collision Detection

#### `checkGridCollision(position, map, occupiedSet, options?)`

- Check if a position collides with walls, boundaries, or occupied cells
- Returns: CollisionResult

#### `checkCollectible(position, map, collectibleChars?)`

- Check if a position contains a collectible tile
- Returns: CollisionResult

#### `buildOccupiedSet(segmentGroups)`

- Build a Set of `"x,y"` strings from multiple arrays of segments
- Returns: Set\<string\>

#### `collidesWithSelfSegments(position, selfSet)`

- Check if a position collides with the entity's own segments
- Returns: boolean

### Bot AI

#### `createStrategyBot<TPlayer, TGame>(config)`

- Create a strategy-based bot with pluggable behavior priorities
- Config: `getHeadPosition`, `getMap`, `getOccupiedPositions`, `getCurrentDirection`, `hasMultipleSegments`, `strategies`, `blockedTiles?`
- Returns: `IBotAI<TPlayer, TGame>`

### Game Lifecycle

#### `handlePauseResume(game, options)`

- Handle pause/resume toggle with configurable countdown
- Returns: `{ changed: boolean }`

#### `handleStartCountdown(game, options)`

- Handle game start countdown logic
- Returns: `{ changed: boolean }`

### Game Loop

#### `GameLoopFactory.create(config?)`

- `tickRateHz`: Default 60
- `measureCPU`: If true, measure CPU usage
- `maxDeltaMs`: Cap delta time to prevent huge jumps
- Returns: IGameLoopRunner

#### `IGameLoopRunner` Methods

- `start()`: Start the loop
- `stop()`: Stop and cleanup
- `pause()`: Pause without stopping
- `resume()`: Resume from pause
- `registerHandler(handler)`: Add tick listener
- `removeHandler(handler)`: Remove tick listener
- `getMetrics()`: Get performance metrics
- `isRunning()`: Check if running

### Movement Types

#### `DIRECTION_VECTORS`

```typescript
{
  'UP': { x: 0, y: -1 },
  'DOWN': { x: 0, y: 1 },
  'LEFT': { x: -1, y: 0 },
  'RIGHT': { x: 1, y: 0 }
}
```

#### `isOppositeDirection(current, next)`

- Check if move is 180° reverse
- Returns: boolean

#### `getDirectionVector(direction)`

- Get Direction Vector for a direction
- Returns: DirectionVector

## Types

All types from `@hrejko/shared` are re-exported for convenience.

### Additional Types in Core

```typescript
// Movement
type DirectionVector = { x: number; y: number };
type MovementContext = { ... };
DIRECTION_VECTORS;
isOppositeDirection();

// Collision
type CollisionType = "none" | "wall" | "entity" | "boundary" | "collectible";
type CollisionResult = { type; targetId?; position?; metadata? };
type CollisionEvent = CollisionResult & { entityId; tick; timestamp };
type GridCollisionInfo = { isBlocked; reason? };

// Events
type TickContext = { deltaMs; tick; currentTime; fps? };
type TickMetrics = { tick; deltaMs; fps; cpuUsagePercent?; memoryMbUsed? };
type GameResult = { status; winnerId?; finalScores; duration; reason? };
type GameSnapshot = { tick; timestamp; data };
type BotContext = { botId; position; gameState; threats; opportunities };

// Bot AI
type IBotAI<TPlayer, TGame> = { decideAction(bot, game): PlayerDirection | null };
type BotStrategy<TPlayer, TGame> = { name; priority; decide(context) };
type StrategyBotConfig<TPlayer, TGame> = { ... };
type BotDecisionContext<TPlayer, TGame> = { ... };

// Game Lifecycle
type PausableGame = { status; pausedAt?; resumeCountdownEndsAt? };
type StartableGame = { status; startCountdownEndsAt?; gameStartTime? };

// Pathfinding
type GridPosition = { x: number; y: number };
type PathfindingOptions = { isWalkable?; wallChars?; maxIterations? };
```

## Best Practices

1. **Use `type` instead of `interface`** — Project convention; all core types use `type` aliases
2. **Use `createBorderedMap()` for new games** — Proven, tested map generation
3. **Always validate connectivity** — Prevents unreachable spawn islands
4. **Use `findRandomSpawnPosition()` for spawns** — Fair player placement
5. **Don't override game loop** — Use `GameLoopFactory` to ensure consistency
6. **Check spawn availability** — Call `countAvailableSpawns()` before game starts
7. **Use `createStrategyBot()` for bot AI** — Pluggable strategy pattern, avoids monolithic AI code
8. **Use `handlePauseResume()` for game lifecycle** — Consistent pause/resume with countdown across games

## Performance Notes

- Map generation: O(size²)
- Connectivity validation: O(size²) BFS
- Spawn finding: O(attempts) for random, O(size²) for exhaustive
- Game loop: ~1% CPU overhead for scheduling

## Troubleshooting

**Q: "Map has unreachable islands"**
A: Regenerate the map. Some random bordered maps can have fragmentation at certain sizes.

**Q: Spawn position returns null**
A: Map is full or no valid spawns. Check `countAvailableSpawns()` before spawning.

**Q: Game loop hickups/lag spikes**
A: Call `loop.getMetrics()` to monitor FPS. Check if handlers have CPU-heavy operations.

## See Also

- [hrejko.claude.md](../../hrejko.claude.md) - Main framework docs
- [snake.claude.md](../examples/top-down-snake/claude.md) - Snake example using core
- [@hrejko/shared](../shared/README.md) - Shared types (re-exported here)
- [@hrejko/server](../server/README.md) - Server implementation
- [@hrejko/client](../client/README.md) - Client implementation