In Part 3, we made the game speed up as the snake grows. Now let us add a friendlier option — wall wrapping. Instead of dying when hitting a wall, the snake passes through and appears on the opposite side. This is the foundation of a "kids mode" where the only way to die is hitting yourself.
Wrapping is simple math. When the snake moves past the right edge (x = 20 on a 20-wide grid), we want it to appear at x = 0. When it moves past the left edge (x = -1), we want it at x = 19. The modulo operator handles both cases.
// Wrapping a value within a range [0, max)
function wrap(value, max) {
return ((value % max) + max) % max;
}
// Examples:
wrap(20, 20); // 0 — went past right edge
wrap(-1, 20); // 19 — went past left edge
wrap(5, 20); // 5 — no wrapping needed
The double-modulo pattern ((value % max) + max) % max is necessary because JavaScript's % operator returns negative values for negative inputs. -1 % 20 gives -1, not 19. Adding max and taking modulo again fixes this.
In the original game, checkCollision() returns true when the head goes out of bounds. With wrapping enabled, we remove the wall check entirely — the snake can never go out of bounds because we wrap its position before checking collisions.
let wallWrap = true; // toggle this to switch modes
function checkCollision() {
const head = snake[0];
// Wall collision only if wrapping is OFF
if (!wallWrap) {
if (head.x < 0 || head.x >= GRID_SIZE ||
head.y < 0 || head.y >= GRID_SIZE) {
return true;
}
}
// Self collision (always active)
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x &&
head.y === snake[i].y) {
return true;
}
}
return false;
}
Self-collision still ends the game. Wall wrapping removes one danger but the snake can still die by running into its own body. This keeps the game challenging even in kids mode.
Apply the wrap function to the new head position right after calculating it. This ensures the head is always within grid bounds before we add it to the snake array.
function moveSnake() {
direction = { ...nextDirection };
const head = snake[0];
let newHead = {
x: head.x + direction.x,
y: head.y + direction.y
};
// Apply wrapping if enabled
if (wallWrap) {
newHead.x = wrap(newHead.x, GRID_SIZE);
newHead.y = wrap(newHead.y, GRID_SIZE);
}
snake.unshift(newHead);
snake.pop();
}
When the snake wraps, add a brief visual flash at the entry and exit points. This helps the player track the snake as it teleports across the screen.
let wrapFlashes = [];
function addWrapFlash(x, y) {
wrapFlashes.push({
x: x * CELL_SIZE + CELL_SIZE / 2,
y: y * CELL_SIZE + CELL_SIZE / 2,
radius: CELL_SIZE,
life: 10
});
}
function detectWrap(oldHead, newHead) {
// If the head jumped more than 1 cell, it wrapped
if (Math.abs(oldHead.x - newHead.x) > 1 ||
Math.abs(oldHead.y - newHead.y) > 1) {
addWrapFlash(oldHead.x, oldHead.y); // exit point
addWrapFlash(newHead.x, newHead.y); // entry point
}
}
function drawWrapFlashes() {
for (let i = wrapFlashes.length - 1; i >= 0; i--) {
const f = wrapFlashes[i];
ctx.save();
ctx.globalAlpha = f.life / 10;
ctx.strokeStyle = '#4cff72';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(f.x, f.y, f.radius * (1 - f.life / 10), 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
f.life--;
if (f.life <= 0) wrapFlashes.splice(i, 1);
}
}
Call detectWrap() in moveSnake() right after calculating the wrapped position, passing the old head and new head. The flash is a green expanding ring that fades out over 10 frames.
Let the player choose between classic (wall death) and wrap mode. A simple key toggle or a menu option works well.
// Toggle with W key before game starts
document.addEventListener('keydown', (e) => {
if (e.key === 'w' || e.key === 'W') {
if (gameOver || !gameStarted) {
wallWrap = !wallWrap;
drawModeIndicator();
}
}
});
function drawModeIndicator() {
const mode = wallWrap ? 'WRAP' : 'CLASSIC';
const color = wallWrap ? '#4cff72' : '#ff4040';
ctx.fillStyle = color;
ctx.font = '10px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Mode: ' + mode, WIDTH / 2, HEIGHT - 6);
}
Kids mode combines wall wrapping with other adjustments to make the game friendlier for younger players: slower speed, a larger grid with bigger cells, and brighter colors.
const KIDS_MODE = {
gridSize: 14, // fewer cells = less precision needed
cellSize: 28, // bigger cells = easier to see
speed: 200, // slower = more reaction time
wallWrap: true, // no wall death
colors: {
snake: '#4cff72',
food: '#ff6b6b',
bg: '#1a1a3e'
}
};
const NORMAL_MODE = {
gridSize: 20,
cellSize: 20,
speed: 150,
wallWrap: false,
colors: {
snake: '#2ad060',
food: '#ff4040',
bg: '#0f0f23'
}
};
function applyMode(mode) {
GRID_SIZE = mode.gridSize;
CELL_SIZE = mode.cellSize;
SPEED = mode.speed;
wallWrap = mode.wallWrap;
canvas.width = GRID_SIZE * CELL_SIZE;
canvas.height = GRID_SIZE * CELL_SIZE;
}
The 14×14 grid with 28px cells gives kids a bigger target area. Combined with 200ms speed and wall wrapping, the game becomes approachable for players as young as 4-5 years old. The only way to lose is running into your own tail, which teaches spatial awareness without the frustration of wall deaths.
Wall wrapping opens up the game for younger players and adds a fun twist for experienced ones. In the next part, we add mobile controls — swipe gestures that let players control the snake on touchscreens.
Continue to Part 5: Mobile Controls →