npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hrejko/core

v1.0.0

Published

Core reusable utilities and base classes for Hrejko games

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(), 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 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 7

6. 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 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

{
  '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

  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