SPIDERBLITZ
:root {
–accent: #f00; /* red accent to match spiders */
}
html, body {
margin: 0;
height: 100%;
background: #ffffff;
color: #000;
font-family: system-ui, -apple-system, Segoe UI, Roboto, “Helvetica Neue”, Arial, “Noto Sans”, “Liberation Sans”, sans-serif;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
/* Page layout */
.game-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
/* Funky title */
/* Simple static title */
.title {
margin: 0;
font-size: 24px;
letter-spacing: 2px;
text-transform: uppercase;
color: #000;
text-shadow: none;
animation: none;
}
.title::before,
.title::after {
content: none;
}
/* Game canvas box */
canvas {
border: 1px solid #000;
image-rendering: pixelated;
image-rendering: crisp-edges;
background: #fff;
}
/* Controls section */
.controls {
font-size: 12px;
line-height: 1.4;
text-align: center;
user-select: none;
}
.controls .row { margin: 2px 0; }
.controls kbd {
display: inline-block;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, “Liberation Mono”, monospace;
font-size: 11px;
border: 1px solid #000;
padding: 1px 4px;
margin: 0 2px;
border-radius: 2px;
background: #fff;
box-shadow: 1px 1px 0 #000;
}
SPIDERBLITZ
/* ———————————————————–
Minimal Pixel Platformer: SPIDERBLITZ
– Infinite horizontally scrolling level
– Platforms ~50% presence; safe higher start platform
– Red spider enemies (~25% of segments; none on spawn screen)
– Space/Up/W = Jump (double jump), Ctrl = Shoot
– No floor; falling off bottom kills you (2s respawn timer)
– Top is open (jump above the top border)
– Lasers kill spiders (slightly larger hitbox) with small explosion
———————————————————– */
const W = 160, H = 120;
const TILE = 8;
const COLS = W / TILE; // 20
const ROWS = H / TILE; // 15
const canvas = document.getElementById(‘game’);
const ctx = canvas.getContext(‘2d’);
ctx.imageSmoothingEnabled = false;
// Input
const keys = new Set();
const KEY_LEFT = [‘ArrowLeft’, ‘a’, ‘A’];
const KEY_RIGHT = [‘ArrowRight’, ‘d’, ‘D’];
const KEY_JUMP = [‘ ‘, ‘ArrowUp’, ‘w’, ‘W’]; // Space + Up/W
const KEY_SHOOT = [‘Control’]; // Ctrl
let prevJumpWanted = false;
let prevShootWanted = false;
addEventListener(‘keydown’, (e) => {
keys.add(e.key);
if ([‘ArrowUp’,’ArrowDown’,’ArrowLeft’,’ArrowRight’,’ ‘].includes(e.key)) e.preventDefault();
}, { passive: false });
addEventListener(‘keyup’, (e) => keys.delete(e.key));
// Camera (horizontal follow)
let cameraX = 0;
// Tick counter (animation/patrol)
let tick = 0;
// Death/respawn state
let isDead = false;
let respawnAt = 0;
// Lasers
const lasers = []; // {x,y,w,h,vx}
const LASER_SPEED = 3.0;
const LASER_W = 6;
const LASER_H = 1;
// Explosion particles
const particles = []; // {x,y,vx,vy,life,color,size}
function spawnExplosion(x, y) {
const cx = x + SPIDER.w / 2;
const cy = y + SPIDER.h / 2;
const count = 10; // small explosion
for (let i = 0; i = 0; i–) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.95;
p.vy = p.vy * 0.95 + 0.03;
p.life–;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawParticles() {
for (const p of particles) {
ctx.fillStyle = p.color;
const sx = Math.round(p.x – cameraX);
const sy = Math.round(p.y);
if (sx W + 2 || sy H + 2) continue;
ctx.fillRect(sx, sy, p.size, p.size);
}
}
// === Infinite World Generation ===
const PLATFORM_ROWS = [ROWS – 5, ROWS – 8, ROWS – 11]; // 10, 7, 4
const SEG_LEN = 6; // columns per generation block
// Forced start platform (safe spawn)
const START_PLATFORM_ROW = ROWS – 8; // higher up
const START_PLATFORM_LEN = 5; // tiles wide at x=0..4
function hash32(n) {
n = (n ^ 61) ^ (n >>> 16);
n = n + (n <>> 4);
n = (n * 0x27d4eb2d) >>> 0;
n = n ^ (n >>> 15);
return n >>> 0;
}
function platformSegmentInfo(b, rRow) {
const h = hash32(b * 1103515245 ^ rRow * 12345);
const present = ((h & 1) === 0); // ~50% chance
const len = present ? (3 + (h % 4)) : 0; // length 3..6
return { present, len, seed: h >>> 0 };
}
// Solid tiles?
function isSolidTile(tc, tr) {
// Top OOB open; bottom OOB open (no floor)
if (tr = ROWS) return false;
// Forced start platform
if (tr === START_PLATFORM_ROW && tc >= 0 && tc = start && tc < start + len;
}
return false;
}
function drawTiles() {
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
const startCol = Math.floor(cameraX / TILE) – 1;
const endCol = startCol + COLS + 3;
for (let c = startCol; c <= endCol; c++) {
const screenX = c * TILE – cameraX;
if (screenX + TILE W) continue;
for (let r = 0; r < ROWS; r++) {
if (isSolidTile(c, r)) {
ctx.strokeRect(screenX, r * TILE, TILE, TILE);
}
}
}
}
// Player
const START_X = TILE * 2;
const START_Y = START_PLATFORM_ROW * TILE – 12; // player.h=12
const player = {
x: START_X,
y: START_Y,
w: 8,
h: 12,
vx: 0,
vy: 0,
onGround: false,
facing: 1, // 1 right, -1 left
runFrame: 0,
runTimer: 0,
jumpCount: 0,
};
// Physics
const ACCEL = 0.12;
const MAX_SPEED = 1.5;
const FRICTION = 0.83;
const GRAVITY = 0.25;
const JUMP_VELOCITY = -3.6;
const RUN_ANIM_SPEED = 8;
// Sprites (8×12)
const SPRITES = {
idle: [
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
".11.11..",
".11.11..",
".11.11..",
"……..",
],
jump: [
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
"…111..",
"…1.1..",
"…1.1..",
"……..",
],
run: [
[
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
"..1.11..",
".11..1..",
"..1..1..",
"……..",
],
[
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
".11..1..",
"..1.11..",
"..1..1..",
"……..",
],
[
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
"..11.1..",
".1…11.",
"..1..1..",
"……..",
],
[
"……..",
"..11….",
".1111…",
".1111…",
"..11….",
".1111…",
".1111…",
".1111…",
".1..11..",
"..11..1.",
"..1..1..",
"……..",
],
]
};
function drawSpriteOutlined(pattern, x, y, flipHorizontal = false) {
const rows = pattern.length;
const cols = pattern[0].length;
const pix = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (pattern[r][c] === '1') {
const cx = flipHorizontal ? (cols – 1 – c) : c;
pix.push({ x: x + cx, y: y + r });
}
}
}
ctx.fillStyle = '#000';
for (const p of pix) {
for (let oy = -1; oy <= 1; oy++) {
for (let ox = -1; ox <= 1; ox++) {
if (ox === 0 && oy === 0) continue;
ctx.fillRect(p.x + ox, p.y + oy, 1, 1);
}
}
}
ctx.fillStyle = '#fff';
for (const p of pix) ctx.fillRect(p.x, p.y, 1, 1);
}
// — Red Spider Enemy —
const SPIDER = {
w: 8,
h: 6,
frames: [
[
"kk….kk",
".k….k.",
"..rrrr..",
".rrrrrr.",
"..rrrr..",
"kk….kk",
],
[
".k….k.",
"kk….kk",
"..rrrr..",
".rrrrrr.",
"..rrrr..",
".k….k.",
],
]
};
function drawSpider(pattern, x, y, flipHorizontal = false) {
const rows = pattern.length;
const cols = pattern[0].length;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ch = pattern[r][c];
if (ch === '.') continue;
const cx = flipHorizontal ? (cols – 1 – c) : c;
ctx.fillStyle = (ch === 'r') ? '#f00' : '#000';
ctx.fillRect(x + cx, y + r, 1, 1);
}
}
}
function pingpong(u, range) {
const m = u % (2 * range);
return m 0) {
const rightTile = Math.floor((newX + p.w – 1) / TILE);
const topTile = Math.floor(p.y / TILE);
const bottomTile= Math.floor((p.y + p.h – 1) / TILE);
for (let tr = topTile; tr <= bottomTile; tr++) {
if (isSolidTile(rightTile, tr)) {
newX = rightTile * TILE – p.w;
p.vx = 0;
break;
}
}
} else {
const leftTile = Math.floor(newX / TILE);
const topTile = Math.floor(p.y / TILE);
const bottomTile= Math.floor((p.y + p.h – 1) / TILE);
for (let tr = topTile; tr 0) {
const bottomTile = Math.floor((newY + p.h – 1) / TILE);
const leftTile = Math.floor(p.x / TILE);
const rightTile = Math.floor((p.x + p.w – 1) / TILE);
for (let tc = leftTile; tc <= rightTile; tc++) {
if (isSolidTile(tc, bottomTile)) {
newY = bottomTile * TILE – p.h;
p.vy = 0;
p.onGround = true;
p.jumpCount = 0;
break;
}
}
} else if (p.vy < 0) {
const topTile = Math.floor(newY / TILE);
const leftTile = Math.floor(p.x / TILE);
const rightTile = Math.floor((p.x + p.w – 1) / TILE);
for (let tc = leftTile; tc <= rightTile; tc++) {
if (isSolidTile(tc, topTile)) {
newY = (topTile + 1) * TILE;
p.vy = 0;
break;
}
}
} else {
const bottomTile = Math.floor((p.y + p.h) / TILE);
const leftTile = Math.floor(p.x / TILE);
const rightTile = Math.floor((p.x + p.w – 1) / TILE);
for (let tc = leftTile; tc keys.has(k));
const right = KEY_RIGHT.some(k => keys.has(k));
const wantsJump = KEY_JUMP.some(k => keys.has(k));
const wantsShoot = KEY_SHOOT.some(k => keys.has(k));
const justPressedJump = wantsJump && !prevJumpWanted;
const justPressedShoot = wantsShoot && !prevShootWanted;
if (left && !right) {
player.vx -= ACCEL;
player.facing = -1;
} else if (right && !left) {
player.vx += ACCEL;
player.facing = 1;
} else {
player.vx *= FRICTION;
if (Math.abs(player.vx) < 0.05) player.vx = 0;
}
player.vx = Math.max(Math.min(player.vx, MAX_SPEED), -MAX_SPEED);
player.vy = Math.min(player.vy + GRAVITY, 4);
if (justPressedJump && (player.onGround || player.jumpCount 0.1 && player.onGround;
if (moving) {
player.runTimer++;
if (player.runTimer % RUN_ANIM_SPEED === 0) {
player.runFrame = (player.runFrame + 1) % SPRITES.run.length;
}
} else {
player.runTimer = 0;
player.runFrame = 0;
}
cameraX = Math.round(player.x + player.w / 2 – W / 2);
}
// Enemy generation
const SAFE_ENEMY_X_MIN = START_X + W; // spawn after one screen
const deadEnemies = new Set(); // key: `${row}:${block}`
function visibleEnemies() {
const enemies = [];
const startCol = Math.floor(cameraX / TILE) – 1;
const endCol = startCol + COLS + 3;
const startBlock = Math.floor(startCol / SEG_LEN);
const endBlock = Math.floor(endCol / SEG_LEN);
for (let b = startBlock; b <= endBlock; b++) {
for (const row of PLATFORM_ROWS) {
const info = platformSegmentInfo(b, row);
if (!info.present || info.len <= 0) continue;
if ((info.seed % 4) !== 0) continue; // ~25% segments
const id = `${row}:${b}`;
if (deadEnemies.has(id)) continue;
const segStartTile = b * SEG_LEN;
const segEndTile = segStartTile + info.len – 1;
const patrolMinX = segStartTile * TILE;
const patrolMaxX = (segEndTile + 1) * TILE – SPIDER.w;
const range = Math.max(0, patrolMaxX – patrolMinX);
if (range <= 0) continue;
if (patrolMaxX > 9) & 1) === 1,
frameIndex: ((tick >> 3) + (info.seed & 3)) % SPIDER.frames.length });
}
}
return enemies;
}
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return !(ax + aw <= bx || bx + bw <= ax || ay + ah <= by || by + bh = 0; i–) {
const L = lasers[i];
L.x += L.vx;
if (L.x – cameraX > W + 24 || L.x + L.w – cameraX < -24) lasers.splice(i, 1);
}
}
function drawLasers() {
ctx.fillStyle = '#000';
for (const L of lasers) {
const sx = Math.round(L.x – cameraX);
const sy = Math.round(L.y);
if (sx + L.w W) continue;
ctx.fillRect(sx, sy, L.w, L.h);
}
}
function updateEnemiesLasersAndCollisions() {
const enemies = visibleEnemies();
// Laser vs Enemy (expanded hitbox for lasers only)
const HIT_PAD = 2;
for (let li = lasers.length – 1; li >= 0; li–) {
const L = lasers[li];
let hit = false;
for (const e of enemies) {
if (deadEnemies.has(e.id)) continue;
const bx = e.x – HIT_PAD;
const by = e.y – HIT_PAD;
const bw = e.w + HIT_PAD * 2;
const bh = e.h + HIT_PAD * 2;
if (rectsOverlap(L.x, L.y, L.w, L.h, bx, by, bw, bh)) {
deadEnemies.add(e.id);
spawnExplosion(e.x, e.y);
hit = true;
break;
}
}
if (hit) lasers.splice(li, 1);
}
// Player vs Enemy (normal hitbox)
for (const e of enemies) {
if (deadEnemies.has(e.id)) continue;
if (rectsOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) {
scheduleRespawn();
break;
}
}
// Draw alive enemies
for (const e of enemies) {
if (deadEnemies.has(e.id)) continue;
const sx = Math.round(e.x – cameraX);
const sy = Math.round(e.y);
if (sx + e.w W) continue;
drawSpider(SPIDER.frames[e.frameIndex], sx, sy, e.flip);
}
}
function scheduleRespawn() {
if (isDead) return;
isDead = true;
respawnAt = performance.now() + 2000;
lasers.length = 0; // clear active beams during death delay
}
function resetToStart() {
player.x = START_X;
player.y = START_Y;
player.vx = 0;
player.vy = 0;
player.onGround = false;
player.jumpCount = 0;
cameraX = 0;
// deadEnemies persist: killed spiders remain gone for their segments
}
function drawPlayer() {
let sprite;
if (!player.onGround) {
sprite = SPRITES.jump;
} else if (Math.abs(player.vx) > 0.1) {
sprite = SPRITES.run[player.runFrame];
} else {
sprite = SPRITES.idle;
}
const screenX = Math.round(player.x – cameraX);
const screenY = Math.round(player.y);
drawSpriteOutlined(sprite, screenX, screenY, player.facing === -1);
}
function loop() {
tick++;
// Clear
ctx.fillStyle = ‘#fff’;
ctx.fillRect(0, 0, W, H);
// World
drawTiles();
if (!isDead) {
updatePlayer();
updateLasers();
updateEnemiesLasersAndCollisions();
drawPlayer();
drawLasers();
updateParticles();
drawParticles();
// Death by falling off bottom
if (player.y > H) scheduleRespawn();
} else {
if (performance.now() >= respawnAt) {
resetToStart();
isDead = false;
}
updateParticles();
drawParticles();
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);