Use arrow keys · Collect the gem · Avoid the goblin · Score: 0
Copy the offline version here into a notepad and save as .html and to open with browser of choice
"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Mini Pixel RPG \u2013 Dynamic Goblin Speed<\/title>\n<style type=\"text\/css\">\n body {\n background: #222;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100vh;\n margin: 0;\n color: #fff;\n font-family: monospace;\n }\n canvas {\n border: 4px solid #fff;\n width: 512px;\n height: 512px;\n image-rendering: pixelated;\n }\n #info {\n margin-top: 1rem;\n }\n<\/style>\n<\/head>\n<body>\n <canvas id=\"game\" width=\"256\" height=\"256\"><\/canvas>\n <div id=\"info\">Use arrow keys \u00b7 Collect the \ud83d\udc8e gem \u00b7 Avoid the \ud83d\udc32 goblin \u00b7 Score: <span id=\"score\">0<\/span><\/div>\n\n<script>\nconst TILE = 16;\nconst MAP_W = 16;\nconst MAP_H = 16;\nconst WALL = 1;\nconst FLOOR = 0;\nconst INITIAL_GOBLIN_MOVE_DELAY = 10;\nconst SPEED_INCREASE_FACTOR = 500;\nconst MIN_GOBLIN_MOVE_DELAY = 3;\n\nfunction createSprite(drawFn) {\n const c = document.createElement('canvas');\n c.width = TILE;\n c.height = TILE;\n const cx = c.getContext('2d');\n drawFn(cx);\n return c;\n}\n\nconst wallSprite = createSprite(cx => {\n cx.fillStyle = '#496C3B';\n cx.fillRect(0, 0, TILE, TILE);\n cx.fillStyle = '#324D2C';\n for (let i = 0; i < 20; i++)\n cx.fillRect(Math.random() * TILE | 0, Math.random() * TILE | 0, 2, 2);\n});\n\nconst floorSprite = createSprite(cx => {\n cx.fillStyle = '#808080';\n cx.fillRect(0, 0, TILE, TILE);\n cx.strokeStyle = '#6a6a6a';\n cx.lineWidth = 1;\n cx.beginPath();\n cx.moveTo(TILE \/ 2, 0);\n cx.lineTo(TILE \/ 2, TILE);\n cx.moveTo(0, TILE \/ 2);\n cx.lineTo(TILE, TILE \/ 2);\n cx.stroke();\n});\n\nconst goblinSprite = createSprite(cx => {\n cx.fillStyle = '#3fa43f';\n cx.fillRect(4, 4, 8, 8);\n cx.fillStyle = '#fff';\n cx.fillRect(5, 6, 2, 2);\n cx.fillRect(9, 6, 2, 2);\n cx.fillStyle = '#000';\n cx.fillRect(6, 10, 4, 1);\n});\n\nconst playerSprite = createSprite(cx => {\n cx.fillStyle = '#f3e0b9';\n cx.fillRect(4, 4, 8, 8);\n cx.fillStyle = '#fff';\n cx.fillRect(5, 6, 2, 2);\n cx.fillRect(9, 6, 2, 2);\n cx.fillStyle = '#000';\n cx.fillRect(6, 10, 4, 1);\n});\n\nconst player = { x: 1, y: 1 };\nconst goblin = { x: MAP_W - 2, y: MAP_H - 2, moveTimer: 0 };\nconst gem = { x: MAP_W - 2, y: 1 };\nlet map = [];\nlet score = 0;\nconst scoreDisplay = document.getElementById('score');\n\nfunction generateMap() {\n map = Array(MAP_W * MAP_H).fill(FLOOR);\n\n for (let x = 0; x < MAP_W; x++) {\n map[0 * MAP_W + x] = WALL;\n map[(MAP_H - 1) * MAP_W + x] = WALL;\n }\n for (let y = 0; y < MAP_H; y++) {\n map[y * MAP_W + 0] = WALL;\n map[y * MAP_W + (MAP_W - 1)] = WALL;\n }\n\n for (let y = 2; y < MAP_H - 2; y++) {\n for (let x = 2; x < MAP_W - 2; x++) {\n if (Math.random() < 0.15) {\n map[y * MAP_W + x] = WALL;\n }\n }\n }\n\n const isPathable = checkPathability(player, gem, map);\n if (!isPathable) {\n generateMap();\n }\n}\n\nfunction checkPathability(start, end, currentMap) {\n const visited = new Set();\n const queue = [start];\n\n while (queue.length > 0) {\n const current = queue.shift();\n const key = `${current.x},${current.y}`;\n\n if (current.x === end.x && current.y === end.y) {\n return true;\n }\n\n if (visited.has(key)) {\n continue;\n }\n visited.add(key);\n\n const neighbors = [\n { x: current.x + 1, y: current.y },\n { x: current.x - 1, y: current.y },\n { x: current.x, y: current.y + 1 },\n { x: current.x, y: current.y - 1 },\n ];\n\n for (const neighbor of neighbors) {\n if (\n neighbor.x >= 0 &&\n neighbor.x < MAP_W &&\n neighbor.y >= 0 &&\n neighbor.y < MAP_H &&\n currentMap[neighbor.y * MAP_W + neighbor.x] === FLOOR\n ) {\n queue.push(neighbor);\n }\n }\n }\n\n return false;\n}\n\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d');\n\nfunction drawTile(x, y, type) {\n if (type === WALL) {\n ctx.drawImage(wallSprite, x * TILE, y * TILE);\n } else {\n ctx.drawImage(floorSprite, x * TILE, y * TILE);\n }\n}\n\nfunction drawGem() {\n ctx.fillStyle = '#ff0';\n ctx.fillRect(gem.x * TILE, gem.y * TILE, TILE, TILE);\n}\n\nfunction drawGoblin() {\n ctx.drawImage(goblinSprite, goblin.x * TILE, goblin.y * TILE);\n}\n\nfunction drawPlayer() {\n ctx.drawImage(playerSprite, player.x * TILE, player.y * TILE);\n}\n\nfunction render() {\n for (let y = 0; y < MAP_H; y++) {\n for (let x = 0; x < MAP_W; x++) {\n drawTile(x, y, map[y * MAP_W + x]);\n }\n }\n drawGem();\n drawGoblin();\n drawPlayer();\n scoreDisplay.textContent = score;\n}\n\nfunction isWalkable(x, y) {\n if (x < 0 || y < 0 || x >= MAP_W || y >= MAP_H) return false;\n return map[y * MAP_W + x] === FLOOR;\n}\n\nwindow.addEventListener('keydown', e => {\n let dx = 0, dy = 0;\n if (e.key === 'ArrowUp') dy = -1;\n else if (e.key === 'ArrowDown') dy = 1;\n else if (e.key === 'ArrowLeft') dx = -1;\n else if (e.key === 'ArrowRight') dx = 1;\n\n const nx = player.x + dx;\n const ny = player.y + dy;\n if (isWalkable(nx, ny)) {\n player.x = nx;\n player.y = ny;\n checkGem();\n }\n});\n\nfunction checkGem() {\n if (player.x === gem.x && player.y === gem.y) {\n score += 100;\n alert(`You found the gem! \ud83c\udf89 Score: ${score}`);\n reset();\n }\n}\n\nfunction reset() {\n player.x = 1;\n player.y = 1;\n goblin.x = MAP_W - 2;\n goblin.y = MAP_H - 2;\n generateMap();\n placeEntities();\n}\n\nfunction placeEntities() {\n if (map[player.y * MAP_W + player.x] === WALL) {\n player.x = 1;\n player.y = 1;\n }\n if (map[goblin.y * MAP_W + goblin.x] === WALL) {\n goblin.x = MAP_W - 2;\n goblin.y = MAP_H - 2;\n }\n\n let newGemX, newGemY;\n do {\n newGemX = Math.floor(Math.random() * (MAP_W - 2)) + 1;\n newGemY = Math.floor(Math.random() * (MAP_H - 2)) + 1;\n } while (map[newGemY * MAP_W + newGemX] === WALL || (newGemX === player.x && newGemY === player.y) || (newGemX === goblin.x && newGemY === goblin.y));\n gem.x = newGemX;\n gem.y = newGemY;\n\n if (!checkPathability(player, gem, map)) {\n reset();\n }\n}\n\nfunction getGoblinMoveDelay() {\n const speedIncreaseLevel = Math.floor(score \/ SPEED_INCREASE_FACTOR);\n let newDelay = INITIAL_GOBLIN_MOVE_DELAY - speedIncreaseLevel;\n return Math.max(newDelay, MIN_GOBLIN_MOVE_DELAY);\n}\n\nfunction moveGoblin() {\n if (goblin.moveTimer > 0) {\n goblin.moveTimer--;\n return;\n }\n\n const dx = Math.sign(player.x - goblin.x);\n const dy = Math.sign(player.y - goblin.y);\n\n const possibleMoves = [];\n if (isWalkable(goblin.x + dx, goblin.y)) {\n possibleMoves.push({ x: goblin.x + dx, y: goblin.y });\n }\n if (isWalkable(goblin.x, goblin.y + dy)) {\n possibleMoves.push({ x: goblin.x, y: goblin.y + dy });\n }\n if (possibleMoves.length > 0) {\n const randomIndex = Math.floor(Math.random() * possibleMoves.length);\n goblin.x = possibleMoves[randomIndex].x;\n goblin.y = possibleMoves[randomIndex].y;\n } else {\n const dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]];\n const viable = dirs.filter(([rdx, rdy]) => isWalkable(goblin.x + rdx, goblin.y + rdy));\n if (viable.length) {\n const [rdx, rdy] = viable[Math.random() * viable.length | 0];\n goblin.x += rdx;\n goblin.y += rdy;\n }\n }\n\n goblin.moveTimer = getGoblinMoveDelay();\n}\n\nfunction checkLose() {\n if (player.x === goblin.x && player.y === goblin.y) {\n alert(`A goblin caught you! \ud83d\udc80 Final Score: ${score}`);\n score = 0;\n reset();\n }\n}\n\nfunction loop() {\n moveGoblin();\n checkLose();\n render();\n requestAnimationFrame(loop);\n}\n\ngenerateMap();\nplaceEntities();\nrender();\nloop();\n<\/script>\n<\/body>\n<\/html>"