Svelte snake desplegado en deno
Svelte Snake es un juego simple escrito en Svelte. Está desplegado en Deno, un entorno de ejecución seguro para JavaScript y TypeScript.

Daniel Gustaw
• 10 min read

Acerca del proyecto
Este tutorial muestra cómo escribir el juego de Serpiente en Svelte. Requiere conocimientos básicos de objetos y métodos de arreglos. En la primera parte, podremos mostrar el mapa, la serpiente y permitir que la serpiente se mueva en la dirección elegida. Este código no será una versión jugable del juego, pero decidí dividir este proyecto en fragmentos por el mayor valor educativo que tiene presentar el proceso de construcción del código, no solo el resultado final.
Crear una aplicación Svelte
Comencemos creando una aplicación Svelte.
npx sv create svelte-snake
cd svelte-snake
Podemos comenzarlo con
pnpm i && pnpm dev
Ahora podemos empezar a escribir el juego.
en src/routes/+page.svelte
establece el código
<script>
let snake = [{ x: 5, y: 5 }];
let direction = { x: 1, y: 0 };
const move = () => {
snake = [{ x: snake[0].x + direction.x, y: snake[0].y + direction.y }, ...snake.slice(0, -1)];
};
setInterval(move, 200);
</script>
<div>
{#each snake as segment}
<div class="snake-segment" style="top: {segment.y * 20}px; left: {segment.x * 20}px;"></div>
{/each}
</div>
<style>
.snake-segment {
position: absolute;
width: 20px;
height: 20px;
background: green;
}
</style>
podemos ver que la serpiente se está moviendo hacia la derecha.
Renderizado del mapa
Ahora podemos renderizar el mapa. Crearemos un mapa de 20x20.
<script>
let snake = [{ x: 5, y: 5 }];
let direction = { x: 1, y: 0 };
const move = () => {
snake = [{ x: snake[0].x + direction.x, y: snake[0].y + direction.y }, ...snake.slice(0, -1)];
};
setInterval(move, 200);
let map = Array.from({ length: 20 }, () => Array.from({ length: 20 }, () => 0));
</script>
<div>
{#each snake as segment}
<div class="snake-segment" style="top: {segment.y * 20}px; left: {segment.x * 20}px;"></div>
{/each}
{#each map as row, y}
{#each row as cell, x}
<div class="map-cell" style="top: {y * 20}px; left: {x * 20}px;"></div>
{/each}
{/each}
</div>
<style>
.snake-segment {
position: absolute;
width: 20px;
height: 20px;
background: green;
}
.map-cell {
position: absolute;
width: 20px;
height: 20px;
background: white;
border: 1px solid black;
}
</style>
Movimiento de la serpiente
Ahora podemos mover la serpiente en la dirección elegida. Estamos usando onMount
para evitar preguntar sobre el objeto window
en el lado del servidor.
<script>
import { onMount } from 'svelte';
let snake = [{ x: 5, y: 5 }];
let direction = { x: 1, y: 0 };
const move = () => {
snake = [{ x: snake[0].x + direction.x, y: snake[0].y + direction.y }, ...snake.slice(0, -1)];
};
setInterval(move, 200);
let map = Array.from({ length: 20 }, () => Array.from({ length: 20 }, () => 0));
onMount(() => {
window.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp') {
direction = {x: 0, y: -1};
} else if (event.key === 'ArrowDown') {
direction = {x: 0, y: 1};
} else if (event.key === 'ArrowLeft') {
direction = {x: -1, y: 0};
} else if (event.key === 'ArrowRight') {
direction = {x: 1, y: 0};
}
});
});
</script>
<div>
{#each map as row, y}
{#each row as cell, x}
<div class="map-cell" style="top: {y * 20}px; left: {x * 20}px;"></div>
{/each}
{/each}
{#each snake as segment}
<div class="snake-segment" style="top: {segment.y * 20}px; left: {segment.x * 20}px;"></div>
{/each}
</div>
<style>
.snake-segment {
position: absolute;
width: 20px;
height: 20px;
background: green;
}
.map-cell {
position: absolute;
width: 20px;
height: 20px;
background: white;
border: 1px solid black;
}
</style>
Serpiente comiendo
Ahora podemos hacer que la serpiente coma comida. Agregaremos un objeto de comida y verificaremos si la cabeza de la serpiente está en la posición de la comida.
Luego generaremos una nueva posición de comida y agregaremos un nuevo segmento a la serpiente sin eliminar el último segmento.
<script>
import { onMount } from 'svelte';
let snake = [{ x: 5, y: 5 }];
let direction = { x: 1, y: 0 };
let food = { x: 10, y: 10 };
const move = () => {
const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
if (head.x === food.x && head.y === food.y) {
food = { x: Math.floor(Math.random() * 20), y: Math.floor(Math.random() * 20) };
snake = [head, ...snake];
} else {
snake = [head, ...snake.slice(0, -1)];
}
};
setInterval(move, 200);
let map = Array.from({ length: 20 }, () => Array.from({ length: 20 }, () => 0));
onMount(() => {
window.addEventListener('keydown', (event) => {
if (event.key === 'ArrowUp') {
direction = {x: 0, y: -1};
} else if (event.key === 'ArrowDown') {
direction = {x: 0, y: 1};
} else if (event.key === 'ArrowLeft') {
direction = {x: -1, y: 0};
} else if (event.key === 'ArrowRight') {
direction = {x: 1, y: 0};
}
});
});
</script>
<div>
{#each map as row, y}
{#each row as cell, x}
<div class="map-cell" style="top: {y * 20}px; left: {x * 20}px;"></div>
{/each}
{/each}
{#each snake as segment}
<div class="snake-segment" style="top: {segment.y * 20}px; left: {segment.x * 20}px;"></div>
{/each}
<div class="food" style="top: {food.y * 20}px; left: {food.x * 20}px;"></div>
</div>
<style>
.snake-segment {
position: absolute;
width: 20px;
height: 20px;
background: green;
}
.map-cell {
position: absolute;
width: 20px;
height: 20px;
background: white;
border: 1px solid black;
}
.food {
position: absolute;
width: 20px;
height: 20px;
background: red;
}
</style>
Detección de colisiones
Para hacer el juego jugable, necesitamos agregar detección de colisiones. Verificaremos si la cabeza de la serpiente está en el borde del mapa o en el cuerpo de la serpiente.
<script lang="ts">
import { onMount } from 'svelte';
let snake = [{ x: 5, y: 5 }];
let direction = { x: 1, y: 0 };
let food = { x: 10, y: 10 };
const gridSize = 20;
let gameOver = false;
const move = () => {
if (gameOver) return;
const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
// Check for collisions with borders
if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {
endGame("You hit the border!");
return;
}
// Check for collisions with the snake itself
if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
endGame("You collided with yourself!");
return;
}
// Check if the snake eats the food
if (head.x === food.x && head.y === food.y) {
food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
snake = [head, ...snake];
} else {
snake = [head, ...snake.slice(0, -1)];
}
};
const endGame = (message: string) => {
gameOver = true;
console.log(message); // Optionally log the message for debugging
};
const resetGame = () => {
gameOver = false;
snake = [{ x: 5, y: 5 }];
direction = { x: 1, y: 0 };
food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
};
setInterval(move, 200);
let map = Array.from({ length: gridSize }, () => Array.from({ length: gridSize }, () => 0));
onMount(() => {
window.addEventListener('keydown', (event) => {
if (gameOver) return;
if (event.key === 'ArrowUp' && direction.y === 0) {
direction = { x: 0, y: -1 };
} else if (event.key === 'ArrowDown' && direction.y === 0) {
direction = { x: 0, y: 1 };
} else if (event.key === 'ArrowLeft' && direction.x === 0) {
direction = { x: -1, y: 0 };
} else if (event.key === 'ArrowRight' && direction.x === 0) {
direction = { x: 1, y: 0 };
}
});
});
</script>
<div>
{#if gameOver}
<div class="overlay">
<p>Game Over!</p>
<button on:click={resetGame}>Restart</button>
</div>
{/if}
{#each map as row, y}
{#each row as cell, x}
<div class="map-cell" style="top: {y * 20}px; left: {x * 20}px;"></div>
{/each}
{/each}
{#each snake as segment}
<div class="snake-segment" style="top: {segment.y * 20}px; left: {segment.x * 20}px;"></div>
{/each}
<div class="food" style="top: {food.y * 20}px; left: {food.x * 20}px;"></div>
</div>
<style>
.snake-segment {
position: absolute;
width: 20px;
height: 20px;
background: green;
}
.map-cell {
position: absolute;
width: 20px;
height: 20px;
background: white;
border: 1px solid black;
}
.food {
position: absolute;
width: 20px;
height: 20px;
background: red;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.overlay button {
margin-top: 10px;
padding: 10px 20px;
background: white;
color: black;
border: none;
cursor: pointer;
font-size: 1rem;
}
</style>
Este es el final del juego clásico de la serpiente. Vamos a agregar más características como puntuación, aumento de velocidad, multijugador, pero primero desplegaremos este juego en Deno Deploy.
Desplegar en Deno
Hay documentación de adaptadores de svelte kit y discusión en reddit.
Tanto la documentación como la discusión no presentan ninguna solución funcional documentada paso a paso.
Hay un panel de control de deno https://dash.deno.com/projects donde puedes seleccionar svelte, pero no funciona por defecto.
Primero necesitamos reemplazar @sveltejs/adapter-auto
con @sveltejs/adapter-static
en svelte.config.js
.
pnpm remove @sveltejs/adapter-auto
pnpm add @sveltejs/adapter-static
en svelte.config.js
cambia
import adapter from "@sveltejs/adapter-auto";
a
import adapter from "@sveltejs/adapter-static";
para hacer que todas las rutas sean completamente pre-renderizables
, añadimos el archivo src/routes/+layout.ts
con el contenido
export const prerender = true;
Ahora, si ejecutamos pnpm build
, tendremos un directorio build
con archivos estáticos.
Para servir desde este directorio, necesitamos un script más: statoc/mod.ts
en el directorio principal.
import { serve } from "https://deno.land/[email protected]/http/mod.ts";
serve((req) => {
const url = new URL(req.url);
const filePath = `${Deno.cwd()}${url.pathname}`;
try {
const file = Deno.readFileSync(filePath);
const contentType = getContentType(url.pathname);
return new Response(file, {
headers: { "content-type": contentType || "application/octet-stream" },
});
} catch {
const file = Deno.readFileSync(`${Deno.cwd()}/index.html`);
return new Response(file, {headers: { "content-type": "text/html" } });
}
});
function getContentType(pathname: string): string | undefined {
const ext = pathname.split(".").pop();
switch (ext) {
case "html": return "text/html";
case "js": return "application/javascript";
case "css": return "text/css";
case "png": return "image/png";
case "jpg": return "image/jpeg";
case "svg": return "image/svg+xml";
case "json": return "application/json";
default: return undefined;
}
}
Configurando deno deploy manualmente, se generará un archivo de flujo de trabajo de github actions .github/workflows/deploy.yml
con el contenido
name: Deploy
on:
push:
branches: main
pull_request:
branches: main
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
permissions:
id-token: write # Needed for auth with Deno Deploy
contents: read # Needed to clone the repository
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Install Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install step
run: "deno install --allow-scripts"
- name: Build step
run: "npm run build"
- name: Upload to Deno Deploy
uses: denoland/deployctl@v1
with:
project: "snake-svelte"
entrypoint: "mod.ts"
root: "build"
entonces mod.ts
en static
se colocará en el directorio build
.
Ahora puedes desplegar simplemente enviando el código al repositorio de github.
Puedes ver el resultado en
https://snake-svelte-ppwa4dcfqbyd.deno.dev/
O para una versión más avanzada, que se describirá en las próximas publicaciones
https://snake-svelte.deno.dev/
Repositorio con el proyecto
https://github.com/gustawdaniel/snake_js
Agregué algunos cambios encss para hacer el juego más jugable, que no son lo suficientemente interesantes como para adjuntarlos aquí, pero puedes revisarlos en commit
Other articles
You can find interesting also.

El impacto de la indexación en el rendimiento de búsqueda en la base de datos MySQL
El uso de índices acelera las búsquedas y aumenta el tamaño de la tabla, mientras que ralentiza las modificaciones. El artículo muestra cómo perfilar consultas y medir el impacto de los índices en el rendimiento de búsqueda.

Daniel Gustaw
• 16 min read

Cómo instalar Yay en una imagen Docker de Arch Linux pura
La instalación de yay requiere algunos pasos como la creación de usuario, la instalación de base-devel y git, cambios en /etc/sudousers, clonar el repositorio de yay y hacer makepkg en él. Esta publicación cubre este proceso paso a paso.

Daniel Gustaw
• 3 min read

Componente de Inicio de Sesión en Nuxt (Rest Strapi)
Ejemplo simple de página de inicio de sesión en nuxt3 escrita como base para copiar y pegar en muchos proyectos similares.

Daniel Gustaw
• 5 min read