A mobile-first roguelike built to be hacked.
Pure JavaScript. Zero dependencies. No build step. Serve the folder, open the page, and start playing. Edit a file, hit refresh, see your changes instantly. This is JavaScript the way it was meant to be: hackable, transparent, and fun.
No npm. No webpack. No babel. No TypeScript. Just pure ES modules that run directly in your browser. Serve the project folder, open the page, and you're playing. Edit src/rules/systems/movementSystem.js, refresh, and your changes are live. No waiting, no compilation, no mysterious build errors.
# Any static HTTP server works. ES modules require it.
python3 -m http.server 8000
# Then open http://localhost:8000This isn't "mobile-friendly" โ it's mobile-native. Touch controls are primary. Designed for phones. Keyboard works great too, but we built this for your thumb on a subway, not a mouse at a desk.
- Tap sides to move (cardinal directions)
- Double-tap to pick up items
- Pinch to zoom
- Swipe right for inventory, down for messages
All UI elements are finger-sized. No hover states. No tiny tap targets. Just intuitive touch controls that work.
Every run is seeded (default: 0xC0FFEE โ). Same seed + same inputs = same outcome, every time. Debug by replaying. Share interesting seeds. Build regression tests that actually work.
const world = new World({ seed: 0xDEADBEEF });
world.tick(1); // Perfectly reproducibleBuilt on a clean Entity-Component-System architecture. Not hidden behind abstractions โ you can see exactly how entities, components, and systems work. Want to understand ECS? Read the source. It's just JavaScript.
- Entities are IDs
- Components are plain objects
- Systems are functions that query and modify components
No magic. No framework ceremony. Just composable logic.
Every file has a single, clear purpose. Want to change how movement works? Open movementSystem.js. Want to add a new monster? Create an archetype in Creatures.js. The codebase is organized for humans, not bundlers.
Deterministic simulation (rules/) is completely separate from rendering (display/). The rules layer has zero DOM, zero rendering, zero async code. It's pure, testable logic. The display layer consumes a stable snapshot and renders however it wants. You can swap renderers, run headless tests, or replay demos without touching game logic.
See SEPARATION_MANIFEST.md for the philosophy.
We're not "shipping a product" โ we're exploiting JavaScript for fun. Clever tricks encouraged. Weird experiments welcome. Open your console, poke around, break things, fix them. This is a playground.
Every decision prioritizes hackability:
- No transpilation (edit and refresh)
- No bundling (import real files)
- No frameworks (see the actual code)
- No abstractions (everything is transparent)
If you can console.log it, you can understand it.
git clone https://github.com/PJensen/JSHack.git
cd JSHack
python3 -m http.server 8000
# Open http://localhost:8000Any static HTTP server works โ ES modules need to be served over HTTP, not opened as file:// URLs. No npm install. No npm run build. Just serve and play.
Touch / Mobile (primary):
- Tap screen sides: Move in that direction
- Double-tap: Pick up items at your feet
- Pinch: Zoom in/out
- Swipe right: Open inventory
- Swipe down: Open message log
Keyboard (also works):
- Arrow keys / WASD / HJKL: Move (pick your poison)
- . (period): Wait a turn
- , (comma): Pick up items
- Q: Drink a potion
- +/- (or numpad): Zoom in/out
- 0: Reset zoom
- X: Camera shake demo (because why not)
On older phones or just want better framerates? Add URL params:
index.html?quality=low # Fast mode: no glow, fewer particles
index.html?quality=high # Full eye candy
index.html?dprCap=1 # Force 1x pixel density (speed boost)
These only affect visuals โ the deterministic simulation stays identical.
src/
rules/ โ Pure deterministic simulation (the roguelike logic)
bridge/ โ Stable contract between rules and display
display/ โ Rendering, particles, camera, input handling
main/ โ Wires everything together
Rules never import Display. Display never imports Rules. They talk through a clean Bridge contract. This keeps the simulation pure and deterministic while letting the visuals do whatever they want.
Strict turn order: player acts โ all monsters act โ effects trigger โ cleanup runs โ back to player. One action per entity per turn. No realtime chaos.
Actions that consume a turn:
- Moving one tile
- Attacking something
- Using an item
- Waiting (yes, waiting is an action)
Attacker rolls: d20 + attackBonus
Target has: armorClass
Hit if roll โฅ AC
Natural 1: always miss
Natural 20: always crit (damage ร critMult)
Damage: roll(minDamage, maxDamage) - defense
Equipment modifies your stats. Affixes add special effects. Crits feel good.
Systems are organized into three phases:
- intents: AI, player input, movement, combat, interactions
- effects: Status effects, equipment bonuses, hunger, mana regen, spawners
- cleanup: Remove dead entities, update spatial index
See scheduler.js for the full registration order. Add your own systems by registering them to a phase. Systems never call other systems โ they emit events instead.
JSHack also has a rules-layer action transaction utility (src/rules/interaction/mutations.js) used by action contexts (item use/apply/eat) for commit-or-cancel behavior.
- Use intents + phases for system ordering and turn flow.
- Use action transactions only for local all-or-nothing mutation batches inside a single action resolver.
- Do not treat action transactions as a second scheduler or engine queue. ECS-js remains the only engine-level deferred command system.
export const Position = Object.freeze({
x: 0,
y: 0,
});
export const Vitality = Object.freeze({
hp: 10,
maxHp: 10,
});That's it. No classes. No inheritance. Just plain data.
import { Goblin } from './rules/archetypes/Creatures.js';
import { createFrom } from './lib/ecs-js/archetype.js';
const goblinId = createFrom(world, Goblin, { x: 10, y: 10 });Archetypes are templates. Spawn as many as you want. Modify their components. They're just entities.
- Open src/rules/archetypes/Creatures.js
- Copy an existing monster definition
- Change the stats (hp, damage, XP, name)
- Refresh your browser
- Your monster spawns
No compilation. No bundling. Just edit and refresh.
-
Create
src/rules/systems/mySystem.js:export function mySystem(world, dt) { for (const [id, pos, thing] of world.query(Position, Thing)) { // Your logic here } }
-
Register it in src/main/scheduler.js:
import { mySystem } from '../rules/systems/mySystem.js'; registerSystem(mySystem, 'intents'); // or 'effects' or 'cleanup'
-
Refresh browser
-
Your system runs every tick
// In your browser console
const world = new World({ seed: 0xC0FFEE });
// ...set up your scenario...
world.tick(1); // Run one turn
world.tick(1); // Run another
// Same seed, same setup โ same results, alwaysReplay bugs. Share seeds. Build regression tests. Determinism is your superpower.
Systems communicate via events, never direct calls:
// โ
GOOD: Emit an event
world.emit('combat:hit', { attackerId, targetId, damage });
// โ BAD: Call another system
combatSystem(world, dt); // Never do this!Events keep the scheduler in control and execution order predictable.
- Turn-based roguelike gameplay โ classic dungeon crawling
- Monster AI โ chase the player, respect obstacles
- Item system โ potions, scrolls, weapons, armor, wands
- Magic system โ fireball, lightning bolt, heal, and more
- Equipment โ weapons, armor, with affix modifiers
- Status effects โ poison, burn, regen, stun
- Hunger system โ eat food or suffer penalties
- Deity favor โ worship gods, gain boons or invoke wrath
- Pet companions โ they follow and fight for you
- Traps โ pressure plates, arrow traps, spike pits
- Shops โ buy and sell items
- Dungeon generation โ procedural levels with rooms and corridors
- FOV & exploration โ shadowcasting visibility, fog of war
- Deterministic replay โ seeded RNG for perfect reproducibility
- Rules profiler โ per-system timing (
?rulesProfile=1) - Event system โ inter-system communication without coupling
- Spatial indexing โ fast radius queries for AI and effects
- Script system โ attach behavior to entities without hardcoding
- Hot reload โ edit JS, refresh browser, see changes instantly
- 26 systems โ movement, combat, AI, items, effects, spawning, cleanup
- 30+ components โ Position, Vitality, Inventory, Brain, Equipment, etc.
- 12 archetype files โ Player, Creatures, Items, Tiles, Doors, Stairs, Traps, etc.
- Spell library โ offensive, defensive, utility spells
- Monster roster โ kobolds, goblins, orcs, trolls, and more
- Item database โ consumables, equipment, treasures
- Deity pantheon โ multiple gods with unique mechanics
All data-driven. All modifiable. All in plain JavaScript files.
JSHack/
โโโ index.html # Entry point (serve over HTTP)
โโโ src/
โ โโโ rules/ # Pure deterministic simulation
โ โ โโโ systems/ # 26 game logic systems
โ โ โโโ components/ # 30+ data containers
โ โ โโโ archetypes/ # Entity templates
โ โ โโโ scripts/ # Behavior hooks (spells, items, traps)
โ โ โโโ data/ # Spells, monsters, items, loot tables
โ โ โโโ environment/ # Dungeon generation, FOV, tiles
โ โโโ bridge/ # Rules โ Display contract
โ โ โโโ schema/ # WorldView, MapView DTOs
โ โโโ display/ # Rendering & presentation
โ โ โโโ passes/ # Render pipeline (glyphs, VFX, particles)
โ โ โโโ camera/ # Camera controller, follow, shake, zoom
โ โ โโโ input/ # Touch & keyboard input routing
โ โ โโโ ui/ # HUD, inventory, messages, overlays
โ โ โโโ palette/ # Visual mappings (glyphs, colors)
โ โโโ main/ # Application wiring
โ โ โโโ scheduler.js # System registration & phases
โ โ โโโ input/ # Input โ Intent conversion
โ โโโ shared/ # Pure utilities (math, grid algorithms)
โ โโโ lib/ # Vendored libraries (ecs-js, deity-js)
โโโ tests/ # Test suite (Deno)
โโโ reference/ # Demos and examples
โโโ AGENTS.md # Guide for AI/autonomous agents
Design principle: Import boundaries enforce separation. Rules can't import Display. Display can't import Rules. Bridge is the contract. See SEPARATION_MANIFEST.md for details.
- Zero build steps โ pure ES modules, instant feedback
- Determinism โ seeded RNG, reproducible runs, testable logic
- Transparency โ no frameworks, no magic, just readable code
- Hackability โ one file = one idea, easy to modify
- Mobile-first โ touch is primary, phones are the platform
- Fun โ we're hacking and exploring JavaScript, not shipping enterprise software
- โ Build tools (webpack, babel, rollup)
- โ Frameworks (React, Vue, Angular)
- โ Dependencies (zero npm packages)
- โ TypeScript (just JavaScript)
- โ Node (we use Deno for tests)
- โ Backwards compatibility hacks (just rework it)
If you can't console.log it and understand it immediately, we're doing it wrong.
Apply effects to entities:
const effects = world.get(entityId, ActiveEffects) || { effects: [] };
effects.effects.push({
key: 'poison', // Effect type
turnsLeft: 5, // Duration
potency: 2, // Damage per turn
});
world.set(entityId, ActiveEffects, effects);Effects tick automatically. Poison deals damage. Regen heals. Stun... stuns. Current statuses are mirrored to the Status component each tick for easy querying.
Attach behavior to entities without hardcoding systems:
// In src/rules/scripts/myScript.js
import { registerScript, ScriptVerb } from '../scripting.js';
registerScript('lightning_wand', {
[ScriptVerb.ItemUse]: (world, ctx) => {
const { userId, targetX, targetY } = ctx;
// Zap logic here
world.emit('damage', { id: targetId, amount: 10 });
}
});
// Attach to entity
world.set(wandId, ScriptRef, { ref: 'lightning_wand' });Scripts respond to verbs: spell:cast, item:use, trap:trigger, affix:onHit, etc.
Systems communicate via events to avoid coupling:
// System A emits
world.emit('combat:hit', { attackerId, targetId, damage });
// System B listens (installed once at startup)
const INSTALLED = Symbol.for('jshack:combatLogger:installed');
if (!world[INSTALLED]) {
world[INSTALLED] = true;
world.on('combat:hit', ({ attackerId, targetId, damage }) => {
console.log(`Entity ${attackerId} hit ${targetId} for ${damage} damage`);
});
}Events flow through the world. Systems stay decoupled. Order is predictable.
Fast radius queries for AI, explosions, AOE:
import { forEachInRadius } from './rules/utils/spatialIndex.js';
forEachInRadius(world, x, y, radius, (entityId) => {
// Apply damage, effects, etc.
});Maintained automatically by spatialIndexSystem in the cleanup phase.
We use Deno (not Node) for testing:
deno test --allow-read tests/
deno run tests/movementSystem.test.jsTests are simple:
import { World } from '../src/lib/ecs-js/index.js';
import { movementSystem } from '../src/rules/systems/movementSystem.js';
const world = new World({ seed: 42 });
// ...setup...
movementSystem(world, 1);
// ...assert...Deterministic seeds mean tests are reproducible. No flaky tests. No "works on my machine."
Contributions that align with the project's vision are welcome. Read CONTRIBUTING.md for setup, guidelines, and expectations. The short version: keep it simple, test your changes, don't break the constraints in TEN_COMMANDMENTS.md.
If you're an autonomous agent or LLM-based copilot reading this:
๐ Read AGENTS.md first. It has everything you need to work with this codebase effectively.
Key rules:
- ECS-js is external; only fix genuine bugs
- No system-to-system calls; use events with Symbol tracking
- Mobile-first always (touch is primary)
- Church (display) and State (rules) are separated
- Deno, not Node
- AGENTS.md โ Guide for AI agents and autonomous operators
- TEN_COMMANDMENTS.md โ Project philosophy and constraints
- SEPARATION_MANIFEST.md โ Layer boundaries and import rules
- ecs-js โ Canonical ECS library (external dependency)
- ecs-js README โ Vendored ECS core API docs
- ecs-js AGENTS.md โ ECS-specific guidance
- Roguelike Development Guide
- ECS Architecture
- Classic roguelikes: NetHack, DCSS, Brogue
Human-Scale Source License (HSSL) v1.2
See LICENSE for terms.
Because JavaScript doesn't need frameworks and build tools to be powerful. Because mobile deserves great roguelikes. Because deterministic simulations are beautiful. Because one file should equal one idea. Because hacking should be fun.
Serve the folder. Edit a file. Refresh. Hack.
That's it. That's the whole pitch.
Now go build something weird. โก
Built with โ (0xC0FFEE) and pure JavaScript.