🎮 Play JSHack · GitHub Pages fallback
A mobile-first roguelike built to be played, studied, and hacked.
Pure JavaScript. Zero dependencies. No build step. Serve the folder, open the page, and play. Edit a file, refresh, and the world changes.
JSHack is both a game and a living source artifact: transparent enough to inspect, simple enough to modify, and strange enough to keep surprising you.
JSHack is a turn-based roguelike about surviving a strange, reactive world one tile at a time.
It chases the lost spirit of older games: consequence, improvisation, mystery, and systems that surprise you back. The difference is that JSHack is built for now: phone-first, browser-native, quick to enter, and dense enough that the dungeon still has secrets after the hundredth run.
You are choosing when to fight, when to run, when to drink the questionable potion, when to sell the loot, when to pray, and when to head back underground.
A run should make sense in your hand: tap to move, swipe into inventory, double-tap to grab what is underfoot, pinch the map when you need a better read. Keyboard controls are there because roguelike players deserve them, but touch is the primary interface.
JSHack is built around readable chaos.
The world should feel systemic, but not opaque. Items should interact. Monsters should behave strangely. Town choices should echo later. A good run should leave behind a story that feels discovered, not scripted.
The goal is not nostalgia for old roguelikes as they were. The goal is to recover what made them dangerous and alive, then rebuild that feeling for modern play.
JSHack is still growing quickly, but it is already a game with enough moving parts to reward player judgment. The technical obsession exists to serve one thing: a world that keeps reacting after the first obvious answer fails.
- Explore procedural dungeons and an overworld village.
- Fight monsters with melee weapons, thrown items, spells, pets, traps, and whatever else you can bend into a tactic.
- Loot, identify, equip, curse, bless, buy, sell, eat, drink, apply, throw, and craft items.
- Manage hunger, carrying weight, mana, status effects, weather, fire, faction trouble, deity favor, and town economy consequences.
- Learn the map through field of view, fog of war, memory, messages, and risk.
- Survive long enough for your build to become a story.
- 9 classes: Warden, Druid, Outlaw, Archeologist, Warlock, Mage, Mireborn, Pilgrim, and Cleric.
- 58 monsters: rats and goblins up through dragons, mind flayers, serpentine multi-tile creatures, mimics, undead, bugs, spellcasters, and stranger dungeon ecology.
- 338 catalog items: 215 equipment entries, 105 magic items, food, materials, seeds, tools, gems, wands, scrolls, potions, weapons, and armor.
- 64 spells: destruction, support, summoning, teleportation, channeling, class abilities, monster magic, wands, scrolls, and gear-driven spell interactions.
- Combat: D&D-style attack rolls, armor class, crits, damage types, resistances, shields, dual wielding, 34 affixes, status procs, gaze attacks, and spell crits.
- World simulation: 12 townfolk roles, shops, economy chains, farming, seasons, lunar calendar, weather, plant growth, fire spread, and harvestable resources.
- Dungeon play: multi-floor procedural dungeons, biome slices, traps, boulder puzzles, chests, stairs, FOV, exploration memory, monster spawners, and 69 status/effect definitions.
- Character pressure: hunger, encumbrance, cursed equipment, unidentified items, pets with behavior, deity boons and wrath, and a score system.
git clone https://github.com/PJensen/JSHack.git
cd JSHack
python3 -m http.server 8000
# Open http://localhost:8000Any static HTTP server works because ES modules need to be served over HTTP, not opened as file:// URLs.
No npm install. No npm run build. Just serve and play.
- 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
- Arrow keys / WASD / HJKL: Move
- . (period): Wait a turn
- , (comma): Pick up items
- Q: Drink a potion
- +/- (or numpad): Zoom in/out
- 0: Reset zoom
- X: Camera shake demo
On older phones or when you 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 for a speed boost
These only affect visuals. The deterministic simulation stays identical.
This is not a desktop roguelike squeezed onto a phone. Touch controls are primary. The game is designed for thumbs, short sessions, and readable decisions on a small screen.
Keyboard works too, because roguelike players deserve it.
Every run is seeded. Same seed plus same inputs means same outcome, every time.
That makes bugs replayable, tests stable, and interesting seeds shareable.
const world = new World({ seed: 0xDEADBEEF });
world.tick(1); // Perfectly reproducibleNo npm. No webpack. No babel. No TypeScript. Just pure ES modules that run directly in the browser.
Serve the folder, open the page, and play. Edit src/rules/systems/movementSystem.js, refresh, and your changes are live.
JSHack is built on a clean Entity-Component-System architecture. Not hidden behind abstractions. You can see exactly how entities, components, and systems work.
- Entities are IDs.
- Components are plain objects.
- Systems are functions that query and modify components.
No framework ceremony. No magic. Just composable logic.
Every file has a single, clear purpose.
Want to change movement? Open movementSystem.js.
Want to add a monster? Create an archetype in Creatures.js.
The codebase is organized for humans, agents, and curiosity — not bundlers.
The deterministic simulation lives in rules/. Rendering, particles, camera, UI, and input live in display/. They talk through bridge/.
The rules layer has zero DOM, zero rendering, and zero async code. It is pure, testable game logic. The display layer consumes stable snapshots and renders them.
See SEPARATION_MANIFEST.md for the philosophy.
JSHack is a playground. Open your console, poke around, break things, fix them.
Every decision prioritizes hackability:
- No transpilation
- No bundling
- No frameworks
- No hidden build system
- No mysterious runtime layer
If you can console.log it, you can understand it.
src/
rules/ — Pure deterministic simulation
bridge/ — Stable contract between rules and display
display/ — Rendering, particles, camera, input handling
main/ — Application wiring
Rules never import Display. Display never imports Rules. They communicate through Bridge contracts.
This keeps the simulation deterministic while letting the presentation layer move freely.
Strict turn order:
player acts → monsters act → effects trigger → cleanup runs → back to player
Actions that consume a turn:
- Moving one tile
- Attacking something
- Using an item
- Waiting
Combat uses D&D-style rolls:
Attacker rolls: d20 + attackBonus
Target has: armorClass
Hit if roll ≥ AC
Natural 1: always miss
Natural 20: always crit
Damage: roll(minDamage, maxDamage) - defense
Equipment modifies your stats. Affixes add special effects. Crits matter.
Systems are organized into three phases:
- intents: AI, player input, movement, combat, interactions
- effects: status effects, equipment bonuses, hunger, mana regen, spawners
- cleanup: dead entity removal, spatial index updates, end-of-turn maintenance
See scheduler.js for the full registration order.
Systems never call other systems directly. They emit events instead.
JSHack also has a rules-layer action transaction utility at src/rules/interaction/mutations.js for commit-or-cancel behavior inside item use, apply, and eat actions.
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,
});No classes. No inheritance. Just 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 are just entities.
- Open src/rules/archetypes/Creatures.js
- Copy an existing monster definition
- Change the stats, XP, glyph, name, and behavior
- 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 the browser. Your system runs every tick.
// In your browser console
const world = new World({ seed: 0xC0FFEE });
// Set up your scenario...
world.tick(1);
world.tick(1);
// Same seed, same setup, same result.Replay 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);Events keep the scheduler in control and execution order predictable.
- Turn-based roguelike gameplay — dungeon survival, overworld trouble, and systems that collide in useful ways
- 9 character classes — Warden, Druid, Outlaw, Archeologist, Warlock, Mage, Mireborn, Pilgrim, Cleric
- Monster AI — 10-level intelligence tiers driving pack alerting, retreat, ambush, and scurry
- 58 monsters across dungeon tiers — rats to dragons, with flying creatures, mimics, serpentine multi-tile creatures, spellcasters, death hooks, and gaze attacks
- Item system — 338 catalog items including potions, scrolls, weapons, armor, wands, gems, food, seeds, and crafting materials
- Magic system — 64 spells across destruction, support, summoning, teleportation, class, monster, and item-driven magic
- Dual-wielding — equip two one-handed weapons with offhand penalties
- Equipment — canonical gear slots with affix modifiers
- Status effects — 69 effect definitions including poison, burn, regen, stun, berserk, curse, confusion, paralysis, and more
- Hunger and survival — eat food or suffer; carry weight and encumbrance matter
- Deity favor — worship gods, gain boons, invoke wrath, and deal with ascetic and dietary tracking
- Pet companions — they follow, fight, and have opinions
- Overworld village — 12 NPC townfolk roles with scheduled daily routines and an economy chain
- Weather system — rain and heavy rain with gameplay effects
- Calendar — 13 lunar months, 8 moon phases, archaic week, 4 seasons
- Plant growth and farming — crops grow through visual stages and harvestable resources regrow
- Quests — quest system with NPC quest givers and economy-driven objectives
- Crafting — alchemy bench, smithing window, cooking with stateful furnace
- Traps — pressure plates, arrow traps, spike pits, and disarming
- Shops — buy and sell items with appraisal pricing
- Dungeon generation — procedural levels with biome slices, Perlin noise, and boulder puzzles
- FOV and exploration — shadowcasting visibility, fog of war, lantern vision
- Identification and curses — unidentified items and cursed gear
- Score system — your performance is quantified
- Fire spread — flames propagate and can be extinguished by rain
- Deterministic replay — seeded RNG for reproducibility
- Rules profiler — per-system timing with
?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
- 2,003 tests across 344 test files — Deno-powered, deterministic, no flakes
- PWA — installable on mobile, no app store required
- Debug console — spawn monsters, inspect state, and reuse prior commands
- 85 systems — movement, combat, AI, items, effects, weather, economy, crafting, spawning, cleanup
- 129 component modules — Position, Vitality, Inventory, Brain, Equipment, WeatherState, CalendarState, TownfolkJob, and more
- 16 archetype files — Player, Creatures, Items, Tiles, Doors, Stairs, Traps, Food, TownGoods, Overworld, and more
- 64 spells — lightning, meteor, blizzard, firestorm, shadow bolt, healing, summoning, teleportation, class abilities, and monster magic
- 58 monsters — rats and goblins up through dragons, mind flayers, mimics, serpentine multi-tile creatures, and ancient wyrms
- 338 catalog items — equipment, magic entries, food, materials, seeds, tools, gems, weapons, armor, potions, scrolls, and wands
- 12 NPC townfolk roles — farmer, smith, miner, barkeep, herbalist, and more
- 4-deity pantheon — gods with unique mechanics, dietary tracking, and ascetic hooks
- 34 affixes and 69 effects — proc gear, resistances, status pressure, and build-defining item behavior
All data-driven. All modifiable. All in plain JavaScript files.
JSHack/
├── index.html # Entry point
├── src/
│ ├── rules/ # Pure deterministic simulation
│ │ ├── systems/ # 85 game logic systems
│ │ ├── components/ # 129 component modules
│ │ ├── archetypes/ # Entity templates
│ │ ├── scripts/ # Behavior hooks
│ │ ├── data/ # Spells, monsters, items, loot tables, calendar
│ │ ├── quests/ # Quest definitions and runtime
│ │ ├── content/ # Interactions, dialog, NPC behaviors
│ │ └── environment/ # Dungeon generation, overworld, FOV, tiles
│ ├── bridge/ # Rules ↔ Display contract
│ │ └── schema/ # WorldView, MapView DTOs
│ ├── display/ # Rendering and presentation
│ │ ├── passes/ # Render pipeline
│ │ ├── fx/ # Weather, projectile, cloud, spell area VFX
│ │ ├── camera/ # Camera controller, follow, shake, zoom
│ │ ├── input/ # Touch and keyboard input routing
│ │ ├── ui/ # HUD, inventory, messages, overlays
│ │ └── palette/ # Visual mappings
│ ├── main/ # Application wiring
│ │ ├── scheduler.js # System registration and phases
│ │ └── input/ # Input → Intent conversion
│ ├── shared/ # Pure utilities
│ └── lib/ # Vendored libraries
├── tests/ # 344 test files, 2,003 tests
├── reference/ # Demos and examples
└── AGENTS.md # Guide for AI/autonomous agents
Import boundaries enforce separation. Rules cannot import Display. Display cannot import Rules. Bridge is the contract.
See SEPARATION_MANIFEST.md for details.
This project believes in:
- Zero build steps — pure ES modules, instant feedback
- Determinism — seeded RNG, reproducible runs, testable logic
- Transparency — no frameworks, no magic, readable code
- Hackability — one file = one idea, easy to modify
- Mobile-first design — touch is primary, phones are the platform
- Fun — hacking, exploring, breaking, fixing, and letting the game stay weird
This project avoids:
- Build tools
- Frameworks
- Dependencies
- TypeScript
- Node runtime requirements
- Backwards compatibility rituals
- Art asset pipelines
Every glyph is Unicode. Zero sprites. Zero textures. Zero asset pipeline.
If you can't console.log it and understand it immediately, something is wrong.
For more on why this project rejects the modern web toolchain, read The Modern Web is a UX Crime Scene.
JSHack shows what native browser tooling can do on its own — and what agentic AI development looks like when the codebase is simple enough for both humans and agents to reason about.
Apply effects to entities:
const effects = world.get(entityId, ActiveEffects) || { effects: [] };
effects.effects.push({
key: 'poison',
turnsLeft: 5,
potency: 2,
});
world.set(entityId, ActiveEffects, effects);Effects tick automatically. Poison damages. 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 such as spell:cast, item:use, trap:trigger, and affix:onHit.
Systems communicate through events:
// System A emits
world.emit('combat:hit', { attackerId, targetId, damage });
// System B listens 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 remains predictable.
Fast radius queries support AI, explosions, and area effects:
import { forEachInRadius } from './rules/utils/spatialIndex.js';
forEachInRadius(world, x, y, radius, (entityId) => {
// Apply damage, effects, etc.
});The spatial index is maintained automatically by spatialIndexSystem in the cleanup phase.
Deno is used for testing:
deno test --allow-read tests/
deno run tests/movementSystem.test.jsPerformance and headless simulation:
deno task bench
deno task headless --turns 500 --report-every 100Headless runtime details and action-schedule format:
Tests 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 });
// Set up scenario...
movementSystem(world, 1);
// Assert result...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, and do not break the constraints in TEN_COMMANDMENTS.md.
If you are an autonomous agent or LLM-based copilot reading this:
Read AGENTS.md first.
It has the operating rules for this codebase.
Key constraints:
- ECS-js is external; only fix genuine bugs.
- No system-to-system calls; use events with Symbol tracking.
- Mobile-first always.
- Rules and display stay 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
- ecs-js README — Vendored ECS core API docs
- ecs-js AGENTS.md — ECS-specific guidance
- Roguelike Development Guide
- ECS Architecture
- Classic roguelikes, especially the parts that make player choice, consequence, and improvisation matter
Human-Scale Source License (HSSL) v1.2
See LICENSE for terms.
Because mobile deserves real roguelikes, not watered-down ones.
Because JavaScript can still be immediate, readable, and powerful without a tower of tooling.
Because deterministic simulations are beautiful.
Because the old dungeon feeling — danger, consequence, improvisation, mystery — still works.
Because hacking should be fun.
Serve the folder. Edit a file. Refresh. Hack.
JSHack is built through agentic development: a human directs the architecture, taste, constraints, and iteration; AI agents do the implementation work.
Agentic development demands architecture, judgment, and relentless iteration. When the codebase is simple enough for both humans and agents to reason about, it works.
Follow development: hackjs.substack.com
Built with ☕ 0xC0FFEE and pure JavaScript.
