// A roguelike floor from a seed, and you can play it. Click the map, // then move with the arrow keys or WASD. Bump a slime to fight it, // pick up gold, find the key, open the chest, take the stairs down. // // One hook here is this page's own: it calls state.on_key(ps, key) // for every key press, with `this` bound to the `state` map below. let tile = 5; let cols = ps.doc_width() / tile; let rows = ps.doc_height() / tile; // The classic LCG. Use the high bits, since the low ones repeat. fn next(s) { (s * 1103515245 + 12345) % 2147483648 } fn pick(s, n) { (s / 65536) % n } // Put `what` on a free floor tile inside room r. Returns the things // and the seed, since a script function only changes its own copies. fn place(things, seed, r, cols, what, avoid) { for k in 0..12 { seed = next(seed); let x = r[0] + 1 + pick(seed, r[2] - 2); seed = next(seed); let y = r[1] + 1 + pick(seed, (r[3] - 2).max(1)); let i = y * cols + x; if things[i] == 0 && i != avoid { things[i] = what; break; } } [things, seed] } // One floor. Tiles: 0 rock, 1 floor, 2 wall. Things: 1 gold, 2 key, // 3 chest, 4 stairs, 5 slime, 6 hurt slime, 7 open chest. // Row 0 belongs to the HUD. fn new_floor(seed, depth, cols, rows) { let tiles = []; tiles.pad(cols * rows, 0); let rooms = []; let tries = 0; while rooms.len() < 6 && tries < 300 { tries += 1; seed = next(seed); let rw = 4 + pick(seed, 5); seed = next(seed); let rh = 3 + pick(seed, 3); seed = next(seed); let rx = 1 + pick(seed, cols - rw - 2); seed = next(seed); let ry = 2 + pick(seed, rows - rh - 3); // Keep a wall's width between rooms. let clear = true; for r in rooms { if rx <= r[0] + r[2] + 1 && r[0] <= rx + rw + 1 && ry <= r[1] + r[3] + 1 && r[1] <= ry + rh + 1 { clear = false; } } if !clear { continue; } for ty in ry..ry + rh { for tx in rx..rx + rw { tiles[ty * cols + tx] = 1; } } rooms.push([rx, ry, rw, rh]); } // Join each room to the one before it with an L-shaped corridor. for i in 1..rooms.len() { let a = rooms[i - 1]; let b = rooms[i]; let x = a[0] + a[2] / 2; let y = a[1] + a[3] / 2; let bx = b[0] + b[2] / 2; let by = b[1] + b[3] / 2; while x != bx { tiles[y * cols + x] = 1; x += if bx > x { 1 } else { -1 }; } while y != by { tiles[y * cols + x] = 1; y += if by > y { 1 } else { -1 }; } } // Walls: rock that touches floor. for ty in 1..rows { for tx in 0..cols { if tiles[ty * cols + tx] != 0 { continue; } let near = false; for dy in -1..=1 { for dx in -1..=1 { let nx = tx + dx; let ny = ty + dy; if nx >= 0 && ny >= 0 && nx < cols && ny < rows && tiles[ny * cols + nx] == 1 { near = true; } } } if near { tiles[ty * cols + tx] = 2; } } } // You start in the first room. Stairs in the last, the chest in the // one before it, the key somewhere else, gold everywhere, slimes in // the rooms between. let first = rooms[0]; let start = [first[0] + 1, first[1] + 1]; let avoid = start[1] * cols + start[0]; let last = rooms.len() - 1; let things = []; things.pad(cols * rows, 0); let chest_room = if last >= 2 { last - 1 } else { last }; let key_room = if last >= 1 { 1 } else { 0 }; let res = place(things, seed, rooms[last], cols, 4, avoid); res = place(res[0], res[1], rooms[chest_room], cols, 3, avoid); res = place(res[0], res[1], rooms[key_room], cols, 2, avoid); for r in rooms { res = place(res[0], res[1], r, cols, 1, avoid); } let slimes = 0; for r in 1..rooms.len() { let s = next(res[1]); if pick(s, 3) == 0 { res[1] = s; continue; } for k in 0..1 + pick(next(s), depth.min(3)) { res = place(res[0], s, rooms[r], cols, 5, avoid); s = res[1]; slimes += 1; } } #{ layout: tiles, things: res[0], rooms: rooms, start: start, slimes: slimes } } // What the knight can see: two tiles around, and the whole room, // walls too, when standing in one. fn reveal(seen, rooms, px, py, cols, rows) { for dy in -2..=2 { for dx in -2..=2 { let x = px + dx; let y = py + dy; if x >= 0 && y >= 1 && x < cols && y < rows { seen[y * cols + x] = 1; } } } for r in rooms { if px >= r[0] && px < r[0] + r[2] && py >= r[1] && py < r[1] + r[3] { for y in r[1] - 1..=r[1] + r[3] { for x in r[0] - 1..=r[0] + r[2] { seen[y * cols + x] = 1; } } } } seen } // Called as state.enter_floor(): `this` is the state map. fn enter_floor() { this.seed = next(this.seed); let fl = new_floor(this.seed, this.depth, this.cols, this.rows); this.layout = fl.layout; this.things = fl.things; this.rooms = fl.rooms; this.px = fl.start[0]; this.py = fl.start[1]; let seen = []; seen.pad(this.cols * this.rows, 0); this.seen = reveal(seen, this.rooms, this.px, this.py, this.cols, this.rows); print(`B${this.depth}: ${fl.rooms.len()} rooms, ${fl.slimes} slimes, a key and a chest`); } fn draw_thing(ps, what, x, y) { if what == 1 { // Gold ps.pixel_fill(x + 1, y + 1, x + 3, y + 3, "#FFD23C"); ps.pixel_set(x + 1, y + 1, "#FFF3B0"); ps.pixel_set(x + 3, y + 3, "#E0861E"); } else if what == 2 { // Key: a ring, a shaft, a tooth ps.pixel_fill(x, y + 1, x + 1, y + 2, "#FFD23C"); ps.pixel_fill(x + 2, y + 2, x + 4, y + 2, "#FFD23C"); ps.pixel_set(x + 4, y + 3, "#FFD23C"); } else if what == 3 || what == 7 { // Chest, shut (gold band) or open (dark inside) ps.pixel_fill(x, y + 1, x + 4, y + 4, "#B5734A"); ps.pixel_fill(x, y + 1, x + 4, y + 1, if what == 3 { "#FFD23C" } else { "#3D1F2E" }); ps.pixel_set(x + 2, y + 3, if what == 3 { "#FFF3B0" } else { "#73402E" }); } else if what == 4 { // Stairs down for k in 0..4 { ps.pixel_fill(x + k, y + k, x + 4, y + k, "#B9B4D8"); } } else if what == 5 || what == 6 { // Slime, paler once hurt let body = if what == 5 { "#3FC26A" } else { "#9BE36B" }; ps.pixel_fill(x + 1, y + 1, x + 3, y + 4, body); ps.pixel_fill(x, y + 3, x + 4, y + 4, body); ps.pixel_set(x + 1, y + 2, "#0A0A1E"); ps.pixel_set(x + 3, y + 2, "#0A0A1E"); } } fn draw_knight(ps, x, y) { ps.pixel_fill(x + 1, y, x + 3, y + 1, "#B9B4D8"); ps.pixel_set(x + 2, y + 1, "#0A0A1E"); ps.pixel_fill(x + 1, y + 2, x + 3, y + 2, "#E02948"); ps.pixel_set(x, y + 2, "#E8E9F2"); ps.pixel_set(x + 4, y + 2, "#FFD23C"); ps.pixel_fill(x + 1, y + 3, x + 3, y + 3, "#A51D45"); ps.pixel_set(x + 1, y + 4, "#5D5590"); ps.pixel_set(x + 3, y + 4, "#5D5590"); } // A 3x5 pixel font: each glyph is 15 bits, read row by row. fn glyph(ps, mask, x, y, c) { for r in 0..5 { for k in 0..3 { if ((mask >> (14 - r * 3 - k)) & 1) == 1 { ps.pixel_set(x + k, y + r, c); } } } } fn draw_number(ps, n, x, y, c) { let font = [31599, 11415, 29671, 29647, 23497, 31183, 31215, 29257, 31727, 31695]; let digits = []; let v = n; loop { digits.insert(0, v % 10); v /= 10; if v == 0 { break; } } for d in digits { glyph(ps, font[d], x, y, c); x += 4; } } // The HUD row: hearts, gold, the key once found, the floor number. fn draw_hud(ps, s) { let w = s.cols * s.tile; ps.pixel_fill(0, 0, w - 1, s.tile - 1, "#14142E"); for i in 0..3 { let c = if i < s.hp { "#E02948" } else { "#433D73" }; let x = 1 + i * 6; ps.pixel_set(x + 1, 0, c); ps.pixel_set(x + 3, 0, c); ps.pixel_fill(x, 1, x + 4, 1, c); ps.pixel_fill(x + 1, 2, x + 3, 2, c); ps.pixel_set(x + 2, 3, c); } ps.pixel_fill(21, 1, 23, 3, "#FFD23C"); draw_number(ps, s.gold, 25, 0, "#FFF3B0"); if s.has_key { draw_thing(ps, 2, 44, -1); } glyph(ps, 27566, w - 14, 0, "#B9B4D8"); draw_number(ps, s.depth.min(99), w - 10, 0, "#B9B4D8"); } fn draw(ps, s) { let t = s.tile; let cols = s.cols; for ty in 1..s.rows { for tx in 0..cols { let i = ty * cols + tx; let x0 = tx * t; let y0 = ty * t; let kind = if s.seen[i] == 0 { 0 } else { s.layout[i] }; if kind == 1 { ps.pixel_fill(x0, y0, x0 + t - 1, y0 + t - 1, if (tx + ty) % 2 == 0 { "#2E2A57" } else { "#29254F" }); draw_thing(ps, s.things[i], x0, y0); } else if kind == 2 { // Wall: a lit top edge, a mortar line, staggered joints. let joint = if ty % 2 == 0 { 1 } else { 3 }; ps.pixel_fill(x0, y0, x0 + t - 1, y0 + t - 1, "#5D5590"); ps.pixel_fill(x0, y0, x0 + t - 1, y0, "#8A82B8"); ps.pixel_fill(x0, y0 + 2, x0 + t - 1, y0 + 2, "#2E2A57"); ps.pixel_set(x0 + joint, y0 + 1, "#2E2A57"); } else { ps.pixel_fill(x0, y0, x0 + t - 1, y0 + t - 1, "#0A0A1E"); } } } draw_knight(ps, s.px * t, s.py * t); draw_hud(ps, s); } // Called as state.on_key(ps, key) with "up", "down", "left", "right". fn on_key(ps, key) { if this.hp <= 0 { this.hp = 3; this.gold = 0; this.has_key = false; this.depth = 1; this.enter_floor(); draw(ps, this); return; } let dx = if key == "left" { -1 } else if key == "right" { 1 } else { 0 }; let dy = if key == "up" { -1 } else if key == "down" { 1 } else { 0 }; if dx == 0 && dy == 0 { return; } let nx = this.px + dx; let ny = this.py + dy; if nx < 0 || ny < 1 || nx >= this.cols || ny >= this.rows { return; } let i = ny * this.cols + nx; // Rock and walls stop you. if this.layout[i] != 1 { return; } let what = this.things[i]; if what == 5 { this.things[i] = 6; print("You hit the slime."); this.seed = next(this.seed); if pick(this.seed, 2) == 0 { this.hp -= 1; print("It splashes back. -1 heart"); } } else if what == 6 { this.things[i] = 1; print("The slime pops and drops a coin."); } else if what == 3 { if this.has_key { this.things[i] = 7; this.has_key = false; this.gold += 10; print("The key fits. +10 gold"); } else { print("The chest is locked. Find the key."); } } else { this.px = nx; this.py = ny; if what == 1 { this.things[i] = 0; this.gold += 1; print(`+1 gold, ${this.gold} in all`); } else if what == 2 { this.things[i] = 0; this.has_key = true; print("You found a key."); } else if what == 4 { this.depth += 1; print("Down the stairs..."); this.enter_floor(); } this.seen = reveal(this.seen, this.rooms, this.px, this.py, this.cols, this.rows); } if this.hp <= 0 { print("You fainted. Press any key to start over."); } draw(ps, this); } let state = #{ tile: tile, cols: cols, rows: rows, seed: 20261003, depth: 1, hp: 3, gold: 0, has_key: false }; state.enter_floor(); draw(ps, state); print("Click the map, then move with the arrow keys or WASD.");