FOSFOR — wąż na terminalu
GryPodgląd
Ładowanie podglądu…
Zbieraj impulsy, wydłużaj węża i omijaj własny ogon. Strzałki, przyciski dotykowe, pauza i wynik.
Kod źródłowy
Licencja MIT — także do użytku komercyjnego. Zachowaj informacje o autorze i licencji.
HTML
<main><header><span class="eyebrow">Wąż</span><h1>FOSFOR_</h1><p>Jeden impuls więcej. Jeden zakręt za późno.</p></header><div class="desktop-split"><section aria-label="Plansza"><canvas id="screen" width="600" height="400" tabindex="0" aria-label="FOSFOR_. Zbieraj kwadraty. Nie dotykaj ścian ani własnego ogona. Steruj strzałkami lub przyciskami.">Gra wymaga obsługi canvas. Wynik i przyciski znajdują się pod planszą.</canvas><div class="controls" aria-label="Sterowanie"><button type="button" data-key="ArrowUp" aria-label="Ruch: góra">↑</button><button type="button" data-key="ArrowLeft" aria-label="Ruch: lewo">←</button><button type="button" data-key="ArrowDown" aria-label="Ruch: dół">↓</button><button type="button" data-key="ArrowRight" aria-label="Ruch: prawo">→</button></div></section><aside><div class="eyebrow">Punkty</div><div id="score" class="metric">0</div><p id="status" class="status" role="status">Naciśnij Graj.</p><div class="actions"><button id="play" type="button">Graj</button><button id="reset" type="button">Od nowa</button></div><p>Zbieraj kwadraty. Nie dotykaj ścian ani własnego ogona. Steruj strzałkami lub przyciskami.</p><small>Esc — pauza. Gra zatrzymuje się po opuszczeniu okna.</small></aside></div></main>
CSS
*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif;background:var(--bg);color:var(--ink)}main{width:min(100% - 32px,980px);margin:32px auto}h1{font-size:clamp(32px,7vw,64px);line-height:1;margin:12px 0 20px;letter-spacing:-.045em}p{line-height:1.55}button{font:inherit;min-height:44px;padding:10px 16px;border:1px solid currentColor;border-radius:0;background:transparent;color:inherit;cursor:pointer}button:hover{background:var(--ink);color:var(--bg)}button:disabled{cursor:default;opacity:.55}button:focus-visible,canvas:focus-visible{outline:3px solid var(--focus);outline-offset:3px}button[aria-pressed="true"]{background:var(--ink);color:var(--bg)}.eyebrow{font:12px ui-monospace,monospace;letter-spacing:.16em;text-transform:uppercase}.status{min-height:48px;font-weight:600}.actions{display:flex;flex-wrap:wrap;gap:8px}.metric{font:700 28px ui-monospace,monospace}small{line-height:1.5}canvas{display:block;width:100%;height:auto;border:1px solid currentColor;touch-action:none}.controls{display:flex;flex-wrap:wrap;gap:8px;margin:14px 0}.controls button{min-width:54px;touch-action:none;user-select:none}.board{display:grid;gap:5px}.board button{padding:0;aspect-ratio:1;min-width:0;font-size:clamp(18px,5vw,30px)}@media(max-width:600px){main{width:calc(100% - 24px);margin:20px auto}h1{margin-bottom:16px}.desktop-split{display:block!important}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
:root{--bg:#071710;--ink:#b7f8b2;--focus:#fff}body{font-family:ui-monospace,monospace}header{border-bottom:1px solid #42724d;margin-bottom:22px}h1{font-family:monospace}.desktop-split{display:grid;grid-template-columns:minmax(0,600px) 1fr;gap:28px}canvas{border-color:#42724d}.metric{font-size:64px}aside{border-left:1px solid #42724d;padding-left:20px}@media(max-width:600px){aside{border-left:0;padding:0}.metric{font-size:40px}}
@media(max-width:600px){aside .metric{font-size:36px}aside .status{min-height:0;margin:8px 0}aside .actions{margin:12px 0}}
JavaScript
function snakeStep(body, vector, food, size) {
const head = { x: body[0].x + vector.x, y: body[0].y + vector.y };
const ate = head.x === food.x && head.y === food.y;
const occupied = ate ? body : body.slice(0, -1);
const dead = head.x < 0 || head.y < 0 || head.x >= size || head.y >= size || occupied.some(p => p.x === head.x && p.y === head.y);
return { dead, ate, body: dead ? body : [head, ...body.slice(0, ate ? body.length : -1)] };
}
$(function () {
const canvas = document.getElementById('screen'), ctx = canvas.getContext('2d'), size = 20;
let body, vector, next, food, points, tick;
function spawn() {
const empty = [];
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!body.some(p => p.x === x && p.y === y)) empty.push({ x, y });
return empty[Math.floor(Math.random() * empty.length)];
}
function init() { body = [{x:8,y:10},{x:7,y:10},{x:6,y:10}]; vector = {x:1,y:0}; next = vector; food = {x:13,y:10}; points = 0; tick = 0; $('#score').text(0); }
function direction(key) {
const vectors = {ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0}}, chosen = vectors[key];
if (chosen && !(chosen.x === -vector.x && chosen.y === -vector.y)) next = chosen;
}
function update(dt) {
tick += dt; if (tick < Math.max(.08, .19 - points * .002)) return; tick = 0; vector = next;
const result = snakeStep(body, vector, food, size);
if (result.dead) { finish('Koniec gry. Wynik: ' + points + '.'); return; }
body = result.body;
if (result.ate) { points += 10; $('#score').text(points); food = spawn(); if (!food) finish('Cała plansza twoja! Wynik: ' + points + '.'); }
}
function draw() {
ctx.fillStyle = '#071710'; ctx.fillRect(0,0,600,400);
for (let y=0;y<20;y++) for(let x=0;x<20;x++){ctx.fillStyle='#143923';ctx.fillRect(102+x*20,2+y*20,16,16);}
ctx.strokeStyle='#70a87b';ctx.strokeRect(100,0,400,400);
if(food){ctx.fillStyle='#fff0a8';ctx.fillRect(103+food.x*20,3+food.y*20,14,14);}
body.forEach((p,i)=>{ctx.fillStyle=i?'#589f64':'#c0ffb8';ctx.fillRect(102+p.x*20,2+p.y*20,16,16);});
}
let playing = false, ended = false, frameId = null, previous = 0;
const held = new Set();
function pause() {
if (!playing) return;
playing = false; held.clear(); cancelAnimationFrame(frameId);
$('#play').text('Wznów'); $('#status').text('Pauza.');
}
function finish(message) {
playing = false; ended = true; held.clear(); cancelAnimationFrame(frameId);
$('#play').text('Zagraj ponownie'); $('#status').text(message);
}
function reset() {
playing = false; ended = false; held.clear(); cancelAnimationFrame(frameId);
init(); draw(); $('#play').text('Graj'); $('#status').text('Naciśnij Graj.');
}
function frame(now) {
if (!playing) return;
const dt = Math.min((now - previous) / 1000, .035); previous = now;
update(dt); draw();
if (playing) frameId = requestAnimationFrame(frame);
}
$('#play').on('click', function () {
if (playing) { pause(); return; }
if (ended) reset();
playing = true; previous = performance.now();
$(this).text('Pauza'); $('#status').text('Gra trwa.'); canvas.focus();
frameId = requestAnimationFrame(frame);
});
$('#reset').on('click', reset);
$(document).on('keydown', function (event) {
if (event.key === 'Escape') { pause(); return; }
if (playing && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
event.preventDefault(); held.add(event.key); direction(event.key);
}
}).on('keyup', event => held.delete(event.key));
$('[data-key]').on('pointerdown', function (event) {
if (!playing) return;
event.preventDefault(); this.setPointerCapture(event.originalEvent.pointerId);
const key = $(this).attr('data-key'); held.add(key); direction(key);
}).on('pointerup pointercancel lostpointercapture', function () { held.delete($(this).attr('data-key')); })
.on('click', function (event) { if (playing && event.originalEvent.detail === 0) direction($(this).attr('data-key')); });
$(window).on('blur', pause);
$(document).on('visibilitychange', function () { if (document.hidden) pause(); });
reset();
});
Edytuj swój pen
Podaj adres e-mail użyty przy zapisie. Wyślemy link do edycji.