@hrejko/core
v1.0.0
Published
Core reusable utilities and base classes for Hrejko games
Maintainers
Readme
@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(),IBotAItype — 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 for details.
Quick Start
1. Map Generation
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
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
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
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)
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 76. Grid Collision Detection
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
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)
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
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
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
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
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 60measureCPU: If true, measure CPU usagemaxDeltaMs: Cap delta time to prevent huge jumps- Returns: IGameLoopRunner
IGameLoopRunner Methods
start(): Start the loopstop(): Stop and cleanuppause(): Pause without stoppingresume(): Resume from pauseregisterHandler(handler): Add tick listenerremoveHandler(handler): Remove tick listenergetMetrics(): Get performance metricsisRunning(): Check if running
Movement Types
DIRECTION_VECTORS
{
'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
// 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
- Use
typeinstead ofinterface— Project convention; all core types usetypealiases - Use
createBorderedMap()for new games — Proven, tested map generation - Always validate connectivity — Prevents unreachable spawn islands
- Use
findRandomSpawnPosition()for spawns — Fair player placement - Don't override game loop — Use
GameLoopFactoryto ensure consistency - Check spawn availability — Call
countAvailableSpawns()before game starts - Use
createStrategyBot()for bot AI — Pluggable strategy pattern, avoids monolithic AI code - 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 - Main framework docs
- snake.claude.md - Snake example using core
- @hrejko/shared - Shared types (re-exported here)
- @hrejko/server - Server implementation
- @hrejko/client - Client implementation
