coledon-snake-game
Version:
A modern Snake game library that can be embedded in any web application with a secret key sequence easter egg
950 lines (949 loc) • 31.6 kB
JavaScript
var O = Object.defineProperty;
var I = (a, e, t) => e in a ? O(a, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : a[e] = t;
var s = (a, e, t) => (I(a, typeof e != "symbol" ? e + "" : e, t), t);
import { Container as d, Graphics as w, TextStyle as T, Text as U, Application as E } from "pixi.js";
const F = {
keys: ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "KeyB", "KeyA"],
timeWindow: 3e3,
caseSensitive: !1
}, c = 20, M = 3, R = 0.2;
class L {
constructor() {
s(this, "container");
s(this, "body", []);
s(this, "direction", "right");
s(this, "moveTimer", 0);
s(this, "graphics");
s(this, "nextDirection", null);
s(this, "color", 65280);
this.container = new d(), this.graphics = new w(), this.container.addChild(this.graphics), this.reset();
}
reset() {
this.body = [], this.direction = "right", this.moveTimer = 0;
for (let e = M - 1; e >= 0; e--)
this.body.push({ x: e * c, y: 0 });
this.draw();
}
update(e) {
this.moveTimer += e / 60, this.moveTimer >= R && (this.move(), this.moveTimer = 0);
}
setDirection(e) {
({
up: "down",
down: "up",
left: "right",
right: "left"
})[e] !== this.direction && e !== this.direction && (this.direction = e);
}
grow() {
const e = this.body[this.body.length - 1];
this.body.push({ ...e });
}
checkFoodCollision(e) {
const t = this.body[0];
return t.x === e.x && t.y === e.y;
}
checkWallCollision() {
const e = this.body[0];
return e.x < 0 || e.x >= 800 || e.y < 0 || e.y >= 600;
}
checkSelfCollision() {
const e = this.body[0];
return this.body.slice(1).some(
(i) => i.x === e.x && i.y === e.y
);
}
setNextDirection(e) {
e && e !== this.direction && (this.nextDirection = e);
}
setColor(e) {
this.color = e, this.draw();
}
move() {
if (this.nextDirection) {
const t = {
up: "down",
down: "up",
left: "right",
right: "left"
};
this.nextDirection !== t[this.direction] && (this.direction = this.nextDirection), this.nextDirection = null;
}
const e = { ...this.body[0] };
switch (this.direction) {
case "up":
e.y -= c;
break;
case "down":
e.y += c;
break;
case "left":
e.x -= c;
break;
case "right":
e.x += c;
break;
}
this.body.unshift(e), this.body.pop(), this.draw();
}
draw() {
this.graphics.clear(), this.body.forEach((e, t) => {
t === 0 ? this.drawSnakeHead(e) : this.drawBodySegment(e, t);
});
}
drawSnakeHead(e) {
const t = c - 2, i = e.x + c / 2, n = e.y + c / 2;
this.graphics.beginFill(this.color), this.graphics.drawRoundedRect(
i - t / 2,
n - t / 2,
t,
t,
6
), this.graphics.endFill(), this.graphics.beginFill(0);
const o = 2;
switch (this.direction) {
case "right":
this.graphics.drawCircle(i + 3, n - 3, o), this.graphics.drawCircle(i + 3, n + 3, o);
break;
case "left":
this.graphics.drawCircle(i - 3, n - 3, o), this.graphics.drawCircle(i - 3, n + 3, o);
break;
case "up":
this.graphics.drawCircle(i - 3, n - 3, o), this.graphics.drawCircle(i + 3, n - 3, o);
break;
case "down":
this.graphics.drawCircle(i - 3, n + 3, o), this.graphics.drawCircle(i + 3, n + 3, o);
break;
}
this.graphics.endFill();
}
drawBodySegment(e, t) {
const i = c - 4, n = e.x + c / 2, o = e.y + c / 2, r = Math.min(t * 0.5, 4), h = i - r, p = this.adjustColorBrightness(this.color, -0.2);
if (this.graphics.beginFill(p), this.graphics.drawRoundedRect(
n - h / 2,
o - h / 2,
h,
h,
4
), this.graphics.endFill(), t % 2 === 0) {
const x = this.adjustColorBrightness(this.color, -0.4);
this.graphics.beginFill(x), this.graphics.drawRoundedRect(
n - h / 4,
o - h / 4,
h / 2,
h / 2,
2
), this.graphics.endFill();
}
}
adjustColorBrightness(e, t) {
const i = e >> 16 & 255, n = e >> 8 & 255, o = e & 255, r = Math.max(0, Math.min(255, i + i * t)), h = Math.max(0, Math.min(255, n + n * t)), p = Math.max(0, Math.min(255, o + o * t));
return r << 16 | h << 8 | p;
}
getDirection() {
return this.direction;
}
getBody() {
return this.body;
}
}
const g = 20, P = 800 / g, $ = 600 / g;
class H {
constructor() {
s(this, "container");
s(this, "position");
s(this, "graphics");
this.container = new d(), this.graphics = new w(), this.container.addChild(this.graphics), this.position = { x: 0, y: 0 }, this.reset([]);
}
// Accepts snakeBody: Position[]
reset(e) {
const t = [];
for (let o = 0; o < P; o++)
for (let r = 0; r < $; r++)
t.push({ x: o * g, y: r * g });
const i = new Set(e.map((o) => `${o.x},${o.y}`)), n = t.filter((o) => !i.has(`${o.x},${o.y}`));
n.length > 0 ? this.position = n[Math.floor(Math.random() * n.length)] : this.position = { x: 0, y: 0 }, this.draw();
}
update(e) {
}
draw() {
this.graphics.clear();
const e = this.position.x + g / 2, t = this.position.y + g / 2, i = (g - 6) / 2;
this.graphics.beginFill(16724787), this.graphics.drawCircle(e, t, i), this.graphics.endFill(), this.graphics.beginFill(16737894, 0.7), this.graphics.drawCircle(e - 2, t - 2, i * 0.6), this.graphics.endFill(), this.graphics.beginFill(16777215, 0.5), this.graphics.drawCircle(e - 3, t - 3, i * 0.3), this.graphics.endFill(), this.graphics.lineStyle(2, 9127187), this.graphics.moveTo(e, t - i + 1), this.graphics.lineTo(e, t - i - 4), this.graphics.lineStyle(0), this.graphics.beginFill(2263842), this.graphics.drawEllipse(e + 2, t - i - 1, 3, 2), this.graphics.endFill();
}
}
const u = class u {
constructor() {
s(this, "currentDirection", "right");
s(this, "nextDirection", null);
s(this, "isInitialized", !1);
}
static getInstance() {
return u.instance || (u.instance = new u()), u.instance;
}
initialize() {
this.isInitialized || (window.addEventListener("keydown", this.handleKeyDown.bind(this)), this.isInitialized = !0);
}
getCurrentDirection() {
return this.currentDirection;
}
getAndConsumeNextDirection() {
const e = this.nextDirection;
return this.nextDirection = null, e;
}
setCurrentDirection(e) {
this.currentDirection = e;
}
handleKeyDown(e) {
const t = {
up: "down",
down: "up",
left: "right",
right: "left"
};
let i = null;
switch (e.key) {
case "ArrowUp":
i = "up";
break;
case "ArrowDown":
i = "down";
break;
case "ArrowLeft":
i = "left";
break;
case "ArrowRight":
i = "right";
break;
}
i && i !== this.currentDirection && i !== t[this.currentDirection] && (this.nextDirection = i);
}
};
s(u, "instance");
let A = u;
class z {
constructor() {
s(this, "container");
s(this, "particles", []);
this.container = new d();
}
burstAt(e, t, i = 16776960, n = 18) {
for (let o = 0; o < n; o++) {
const r = Math.PI * 2 * o / n + Math.random() * 0.2, h = 2 + Math.random() * 2, p = Math.cos(r) * h, x = Math.sin(r) * h, m = new w();
m.beginFill(i), m.drawCircle(0, 0, 3 + Math.random() * 2), m.endFill(), m.x = e, m.y = t, this.container.addChild(m), this.particles.push({ gfx: m, vx: p, vy: x, life: 30 + Math.random() * 10 });
}
}
update() {
for (let e = this.particles.length - 1; e >= 0; e--) {
const t = this.particles[e];
t.gfx.x += t.vx, t.gfx.y += t.vy, t.vx *= 0.95, t.vy *= 0.95, t.life--, t.gfx.alpha = Math.max(0, t.life / 40), t.life <= 0 && (this.container.removeChild(t.gfx), t.gfx.destroy(), this.particles.splice(e, 1));
}
}
}
const l = 20, K = 800 / l, B = 600 / l;
class N {
constructor() {
s(this, "container");
s(this, "position");
s(this, "graphics");
s(this, "isActive", !1);
s(this, "animationTimer", 0);
this.container = new d(), this.graphics = new w(), this.container.addChild(this.graphics), this.position = { x: 0, y: 0 }, this.hide();
}
spawn(e, t) {
const i = [];
for (let r = 0; r < K; r++)
for (let h = 0; h < B; h++)
i.push({ x: r * l, y: h * l });
const n = new Set(e.map((r) => `${r.x},${r.y}`));
n.add(`${t.x},${t.y}`);
const o = i.filter((r) => !n.has(`${r.x},${r.y}`));
o.length > 0 ? (this.position = o[Math.floor(Math.random() * o.length)], this.isActive = !0, this.animationTimer = 0, this.draw(), this.container.visible = !0) : this.hide();
}
hide() {
this.isActive = !1, this.container.visible = !1;
}
update(e) {
this.isActive && (this.animationTimer += e, this.draw());
}
draw() {
this.graphics.clear();
const e = this.position.x + l / 2, t = this.position.y + l / 2, i = Math.sin(this.animationTimer * 0.2) * 0.3 + 0.7;
this.graphics.beginFill(6737151, 0.3 * i), this.graphics.drawCircle(e, t, l / 2 + 2), this.graphics.endFill(), this.graphics.beginFill(16776960);
const n = [
e - 3,
t - 8,
// Top left
e + 1,
t - 8,
// Top right
e - 2,
t - 2,
// Middle left
e + 4,
t - 2,
// Middle right
e + 1,
t + 2,
// Lower middle right
e + 6,
t + 8,
// Bottom right
e + 2,
t + 8,
// Bottom left
e + 3,
t + 2,
// Lower middle left
e - 2,
t + 2,
// Lower left
e - 6,
t - 4
// Upper left
];
this.graphics.drawPolygon(n), this.graphics.endFill(), this.graphics.beginFill(16777215, 0.8);
const o = [
e - 1,
t - 6,
e + 1,
t - 6,
e - 1,
t - 1,
e + 2,
t - 1,
e + 1,
t + 1,
e + 4,
t + 6,
e + 2,
t + 6,
e + 2,
t + 1,
e - 1,
t + 1,
e - 4,
t - 2
];
if (this.graphics.drawPolygon(o), this.graphics.endFill(), Math.random() < 0.3) {
this.graphics.beginFill(65535, 0.8);
for (let r = 0; r < 3; r++) {
const h = e + (Math.random() - 0.5) * l, p = t + (Math.random() - 0.5) * l;
this.graphics.drawCircle(h, p, 1);
}
this.graphics.endFill();
}
}
}
class _ {
// frames
constructor() {
s(this, "container");
s(this, "popupText");
s(this, "timer", 0);
s(this, "duration", 60);
this.container = new d();
const e = new T({
fontFamily: "Arial",
fontSize: 40,
fill: "#ffe066",
stroke: "#000",
strokeThickness: 5,
align: "center",
dropShadow: !0,
dropShadowColor: "#333",
dropShadowBlur: 4,
dropShadowDistance: 2
});
this.popupText = new U("", e), this.popupText.anchor.set(0.5), this.popupText.position.set(400, 100), this.popupText.visible = !1, this.container.addChild(this.popupText);
}
show(e) {
this.popupText.text = e, this.popupText.visible = !0, this.timer = this.duration, this.popupText.alpha = 1;
}
update() {
this.popupText.visible && (this.timer--, this.timer < 20 && (this.popupText.alpha = this.timer / 20), this.timer <= 0 && (this.popupText.visible = !1));
}
}
const k = [
{
name: "Dark",
background: 1710618,
snake: 65280,
food: 16729156,
powerUp: 3381759,
particle: 16769126,
uiText: "#fff",
combo: "#ffe066"
},
{
name: "Light",
background: 16316664,
snake: 2263091,
food: 14492194,
powerUp: 2258892,
particle: 16761600,
uiText: "#222",
combo: "#ffb700"
},
{
name: "Retro",
background: 2236996,
snake: 65535,
food: 16711935,
powerUp: 16776960,
particle: 65535,
uiText: "#00ffff",
combo: "#ff00ff"
}
];
let v = 2;
const G = [], f = {
getTheme() {
return k[v];
},
nextTheme() {
v = (v + 1) % k.length, G.forEach((a) => a(k[v]));
},
onThemeChange(a) {
G.push(a);
}
}, b = [
{
name: "Classic",
color: 65280,
particle: 16769126
},
{
name: "Neon",
color: 65535,
particle: 16711935
},
{
name: "Rainbow",
color: 16711680,
// Will cycle in code
particle: 16777215
},
{
name: "Retro",
color: 16776960,
particle: 65535
}
];
let S = 0;
const D = [], C = {
getSkin() {
return b[S];
},
nextSkin() {
S = (S + 1) % b.length, D.forEach((a) => a(b[S]));
},
onSkinChange(a) {
D.push(a);
}
};
class W {
constructor() {
s(this, "container");
s(this, "achievements");
s(this, "gameStats");
s(this, "onAchievementUnlocked");
this.container = new d(), this.container.visible = !1, this.gameStats = {
score: 0,
level: 1,
survivalTime: 0,
comboCount: 0,
powerUpCount: 0
}, this.achievements = [
{
id: "first-food",
name: "First Food",
description: "Eat your first food",
isUnlocked: !1,
condition: (e) => e.score >= 10
},
{
id: "score-100",
name: "Score 100",
description: "Reach 100 points",
isUnlocked: !1,
condition: (e) => e.score >= 100
},
{
id: "score-500",
name: "Score 500",
description: "Reach 500 points",
isUnlocked: !1,
condition: (e) => e.score >= 500
},
{
id: "level-5",
name: "Level 5",
description: "Reach level 5",
isUnlocked: !1,
condition: (e) => e.level >= 5
},
{
id: "combo-master",
name: "Combo Master",
description: "Get a 10x combo",
isUnlocked: !1,
condition: (e) => e.comboCount >= 10
},
{
id: "power-user",
name: "Power User",
description: "Collect 5 power-ups",
isUnlocked: !1,
condition: (e) => e.powerUpCount >= 5
}
];
}
setOnAchievementUnlocked(e) {
this.onAchievementUnlocked = e;
}
updateScore(e) {
this.gameStats.score = e, this.checkAchievements();
}
updateLevel(e) {
this.gameStats.level = e, this.checkAchievements();
}
updateSurvivalTime() {
this.gameStats.survivalTime++, this.checkAchievements();
}
updateComboCount() {
this.gameStats.comboCount++, this.checkAchievements();
}
updatePowerUpCount() {
this.gameStats.powerUpCount++, this.checkAchievements();
}
checkAchievements() {
for (const e of this.achievements)
!e.isUnlocked && e.condition(this.gameStats) && (e.isUnlocked = !0, this.onAchievementUnlocked && this.onAchievementUnlocked(e.name));
}
getUnlockedAchievements() {
return this.achievements.filter((e) => e.isUnlocked).map((e) => e.name);
}
resetGameSession() {
this.gameStats = {
score: 0,
level: 1,
survivalTime: 0,
comboCount: 0,
powerUpCount: 0
};
}
}
class q {
constructor() {
s(this, "container");
s(this, "graphics");
s(this, "titleText");
s(this, "restartText");
s(this, "onRestart", null);
this.container = new d(), this.container.visible = !1, this.createOverlay();
}
createOverlay() {
this.graphics = new w(), this.graphics.beginFill(0, 0.8), this.graphics.drawRect(0, 0, 800, 600), this.graphics.endFill(), this.container.addChild(this.graphics);
const e = new T({
fontFamily: "Arial",
fontSize: 48,
fill: "#ff4444",
fontWeight: "bold"
});
this.titleText = new U("GAME OVER", e), this.titleText.anchor.set(0.5), this.titleText.position.set(400, 250), this.container.addChild(this.titleText);
const t = new T({
fontFamily: "Arial",
fontSize: 24,
fill: "#ffffff"
});
this.restartText = new U("Press SPACE to restart", t), this.restartText.anchor.set(0.5), this.restartText.position.set(400, 320), this.container.addChild(this.restartText);
}
show(e) {
this.container.visible = !0, this.onRestart = e;
const t = (i) => {
i.code === "Space" && (this.hide(), this.onRestart && this.onRestart(), window.removeEventListener("keydown", t));
};
window.addEventListener("keydown", t);
}
hide() {
this.container.visible = !1;
}
}
class Y {
constructor(e, t) {
s(this, "app");
s(this, "gameContainer");
s(this, "snake");
s(this, "food");
s(this, "powerUp");
s(this, "gameState");
s(this, "isRunning", !1);
s(this, "isPaused", !1);
s(this, "inputManager");
s(this, "externalUI");
s(this, "gameOverOverlay");
s(this, "particles");
s(this, "powerUpTimer", 0);
s(this, "powerUpActive", !1);
s(this, "powerUpDuration", 300);
// 5 seconds at 60fps
s(this, "powerUpChance", 0.01);
// probability per frame
s(this, "comboPopup");
s(this, "comboCount", 0);
// food eaten in combo window
s(this, "comboTimer", 0);
// time remaining for combo
s(this, "comboWindow", 1800);
// 30 seconds at 60fps
s(this, "currentSkin");
s(this, "achievements");
this.app = e, this.externalUI = t, this.gameContainer = new d(), this.app.stage.addChild(this.gameContainer), this.gameState = {
score: 0,
level: 1,
speed: 1,
isGameOver: !1
}, this.snake = new L(), this.food = new H(), this.powerUp = new N(), this.gameContainer.addChild(this.snake.container), this.gameContainer.addChild(this.food.container), this.gameContainer.addChild(this.powerUp.container), this.gameOverOverlay = new q(), this.app.stage.addChild(this.gameOverOverlay.container), this.particles = new z(), this.app.stage.addChild(this.particles.container), this.comboPopup = new _(), this.app.stage.addChild(this.comboPopup.container), f.onThemeChange((i) => this.applyTheme(i)), window.addEventListener("keydown", (i) => {
i.key.toLowerCase() === "t" && this.nextTheme();
}), this.currentSkin = C.getSkin(), this.snake.setColor(this.currentSkin.color), C.onSkinChange((i) => {
this.currentSkin = i, this.snake.setColor(i.color), this.externalUI.updateSkin(i.name);
}), window.addEventListener("keydown", (i) => {
i.key.toLowerCase() === "s" && this.nextSkin();
}), window.addEventListener("keydown", (i) => {
i.code === "Space" ? (i.preventDefault(), this.togglePause()) : i.code === "Escape" && (i.preventDefault(), this.restart());
}), this.achievements = new W(), this.achievements.setOnAchievementUnlocked((i) => {
this.externalUI.unlockAchievement(i);
}), this.app.stage.addChild(this.achievements.container), this.inputManager = A.getInstance(), this.inputManager.initialize(), this.applyTheme(f.getTheme()), this.externalUI.updateTheme(f.getTheme().name), this.externalUI.updateSkin(this.currentSkin.name), this.externalUI.updateGameStatus("ready"), this.app.ticker.add(this.update.bind(this)), console.log("[Game] Constructor complete, ticker added");
}
nextTheme() {
f.nextTheme(), this.externalUI.updateTheme(f.getTheme().name);
}
nextSkin() {
C.nextSkin();
}
togglePause() {
!this.isRunning || this.gameState.isGameOver || (this.isPaused = !this.isPaused, this.externalUI.updateGameStatus(this.isPaused ? "paused" : "playing"), console.log(`[Game] ${this.isPaused ? "Paused" : "Resumed"}`));
}
restart() {
(this.isRunning || this.gameState.isGameOver) && this.start();
}
start() {
console.log("[Game] Start called"), this.isRunning = !0, this.isPaused = !1, this.gameState.isGameOver = !1, this.gameState.score = 0, this.gameState.level = 1, this.gameState.speed = 1, this.comboCount = 0, this.comboTimer = 0, this.powerUpTimer = 0, this.powerUpActive = !1, this.powerUp.hide(), this.snake.reset(), this.food.reset(this.snake.getBody()), this.gameOverOverlay.hide(), this.externalUI.updateScore(this.gameState.score), this.externalUI.updateLevel(this.gameState.level), this.externalUI.updateGameStatus("playing"), this.achievements.resetGameSession(), console.log("[Game] Start complete, isRunning:", this.isRunning);
}
update(e) {
if (!this.isRunning || this.gameState.isGameOver || this.isPaused)
return;
const t = this.inputManager.getAndConsumeNextDirection();
this.snake.setNextDirection(t), !this.powerUp.isActive && !this.powerUpActive && Math.random() < this.powerUpChance && this.powerUp.spawn(this.snake.getBody(), this.food.position), this.snake.update(e * this.gameState.speed), this.inputManager.setCurrentDirection(this.snake.getDirection()), this.food.update(e), this.powerUp.update(e), this.particles.update(), this.comboPopup.update(), this.comboTimer > 0 && this.comboTimer--, this.comboTimer === 0 && (this.comboCount = 0), this.powerUpActive && (this.powerUpTimer--, this.powerUpTimer <= 0 && (this.gameState.speed = 1 + (this.gameState.level - 1) * 0.2, this.powerUpActive = !1)), this.checkCollisions(), this.externalUI.updateScore(this.gameState.score), this.externalUI.updateLevel(this.gameState.level), this.achievements.updateSurvivalTime();
}
checkCollisions() {
if (this.snake.checkFoodCollision(this.food.position)) {
this.snake.grow(), this.comboTimer > 0 ? this.comboCount++ : this.comboCount = 1, this.comboTimer = this.comboWindow;
let e = 0;
this.comboCount > 0 && this.comboCount % 5 === 0 && (e = this.comboCount * 10, console.log(`[Combo] ${this.comboCount} foods in 30s! +${e}`), this.comboPopup.show(`${this.comboCount} foods in 30s! +${e}`), this.particles.burstAt(this.food.position.x, this.food.position.y, this.currentSkin.particle, 48), this.achievements.updateComboCount()), this.particles.burstAt(this.food.position.x, this.food.position.y, this.currentSkin.particle), this.food.reset(this.snake.getBody()), this.gameState.score += 10 + e, this.achievements.updateScore(this.gameState.score), this.checkLevelUp();
}
this.powerUp.isActive && this.snake.getBody()[0].x === this.powerUp.position.x && this.snake.getBody()[0].y === this.powerUp.position.y && (this.powerUp.hide(), this.powerUpActive = !0, this.powerUpTimer = this.powerUpDuration, this.gameState.speed = 2 + (this.gameState.level - 1) * 0.2, this.particles.burstAt(this.powerUp.position.x, this.powerUp.position.y, 6737151), this.achievements.updatePowerUpCount()), (this.snake.checkWallCollision() || this.snake.checkSelfCollision()) && this.gameOver();
}
checkLevelUp() {
const e = Math.floor(this.gameState.score / 100) + 1;
e > this.gameState.level && (this.gameState.level = e, this.gameState.speed = 1 + (this.gameState.level - 1) * 0.2, this.externalUI.updateLevel(this.gameState.level), this.achievements.updateLevel(this.gameState.level), console.log(`[Game] Level up! New level: ${this.gameState.level}, speed: ${this.gameState.speed}`));
}
gameOver() {
console.log("[Game] Game over!");
const e = this.gameState.score;
this.isRunning = !1, this.isPaused = !1, this.gameState.isGameOver = !0, this.externalUI.updateGameStatus("game-over"), this.externalUI.gameCompleted(e), this.particles.burstAt(this.snake.getBody()[0].x, this.snake.getBody()[0].y, 16711680, 36), this.gameOverOverlay.show(() => {
this.start();
});
}
applyTheme(e) {
this.app.renderer.background.color = e.background;
}
}
class y {
/**
* Set a cookie with no expiration date (persistent)
*/
static setCookie(e, t) {
const i = /* @__PURE__ */ new Date();
i.setFullYear(i.getFullYear() + 10), document.cookie = `${e}=${encodeURIComponent(t)}; expires=${i.toUTCString()}; path=/; SameSite=Strict`;
}
/**
* Get a cookie value by name
*/
static getCookie(e) {
const t = e + "=", i = document.cookie.split(";");
for (let n = 0; n < i.length; n++) {
let o = i[n];
for (; o.charAt(0) === " "; )
o = o.substring(1, o.length);
if (o.indexOf(t) === 0)
return decodeURIComponent(o.substring(t.length, o.length));
}
return null;
}
/**
* Load game statistics from cookies
*/
static loadStats() {
try {
const e = this.getCookie(this.COOKIE_NAME);
if (e) {
const t = JSON.parse(e);
return {
...this.DEFAULT_STATS,
...t
};
}
} catch (e) {
console.warn("[CookieManager] Error loading stats from cookies:", e);
}
return { ...this.DEFAULT_STATS };
}
/**
* Save game statistics to cookies
*/
static saveStats(e) {
try {
const t = JSON.stringify(e);
this.setCookie(this.COOKIE_NAME, t), console.log("[CookieManager] Stats saved:", e);
} catch (t) {
console.error("[CookieManager] Error saving stats to cookies:", t);
}
}
/**
* Record a completed game
*/
static recordGame(e) {
const t = this.loadStats(), i = {
gamesPlayed: t.gamesPlayed + 1,
highestScore: Math.max(t.highestScore, e),
totalScore: t.totalScore + e,
averageScore: 0,
// Will be calculated below
lastPlayed: (/* @__PURE__ */ new Date()).toISOString()
};
return i.averageScore = Math.round(i.totalScore / i.gamesPlayed), this.saveStats(i), i;
}
/**
* Update highest score if current score is higher
*/
static updateHighScore(e) {
const t = this.loadStats();
return e > t.highestScore ? (t.highestScore = e, this.saveStats(t), !0) : !1;
}
/**
* Get formatted statistics for display
*/
static getFormattedStats() {
const e = this.loadStats();
return {
gamesPlayed: e.gamesPlayed.toLocaleString(),
highestScore: e.highestScore.toLocaleString(),
averageScore: e.averageScore.toLocaleString(),
lastPlayed: e.lastPlayed ? new Date(e.lastPlayed).toLocaleDateString() : "Never"
};
}
/**
* Reset all statistics (for testing or user preference)
*/
static resetStats() {
this.saveStats({ ...this.DEFAULT_STATS }), console.log("[CookieManager] All stats reset");
}
/**
* Check if this is the first time playing
*/
static isFirstTime() {
return this.loadStats().gamesPlayed === 0;
}
}
s(y, "COOKIE_NAME", "snake_game_stats"), s(y, "DEFAULT_STATS", {
gamesPlayed: 0,
highestScore: 0,
totalScore: 0,
averageScore: 0,
lastPlayed: ""
});
class j {
constructor(e = {}) {
s(this, "config");
s(this, "keySequenceDetector");
s(this, "gameOverlay", null);
s(this, "pixiApp", null);
s(this, "game", null);
s(this, "isGameOpen", !1);
this.config = {
container: document.body,
keySequence: F,
width: 800,
height: 600,
zIndex: 1e4,
autoStart: !1,
closeOnEscape: !0,
showInstructions: !0,
onGameOpen: () => {
},
onGameClose: () => {
},
onHighScore: () => {
},
...e
}, this.keySequenceDetector = new X(
this.config.keySequence,
() => this.toggleGame()
), this.config.autoStart && this.openGame();
}
/**
* Manually trigger the game (bypassing key sequence)
*/
openGame() {
this.isGameOpen || (this.createGameOverlay(), this.initializeGame(), this.isGameOpen = !0, this.config.onGameOpen());
}
/**
* Close the game
*/
closeGame() {
this.isGameOpen && (this.destroyGameOverlay(), this.isGameOpen = !1, this.config.onGameClose());
}
/**
* Toggle game open/close
*/
toggleGame() {
this.isGameOpen ? this.closeGame() : this.openGame();
}
/**
* Destroy the easter egg instance and clean up
*/
destroy() {
this.keySequenceDetector.destroy(), this.closeGame();
}
/**
* Update configuration
*/
updateConfig(e) {
this.config = { ...this.config, ...e };
}
createGameOverlay() {
this.gameOverlay = document.createElement("div"), this.gameOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: ${this.config.zIndex};
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-family: Arial, sans-serif;
`;
const e = document.createElement("div");
e.id = "snake-easter-egg-container", e.style.cssText = `
position: relative;
border: 3px solid #333;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
`;
const t = document.createElement("button");
if (t.textContent = "×", t.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border: none;
background: rgba(255, 255, 255, 0.2);
color: white;
font-size: 18px;
cursor: pointer;
border-radius: 50%;
z-index: 1;
`, t.addEventListener("click", () => this.closeGame()), this.config.showInstructions) {
const n = document.createElement("div");
n.style.cssText = `
color: white;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
`, n.innerHTML = `
<p>🐍 Hidden Snake Game Discovered!</p>
<p>Use arrow keys to control • Press ESC to close</p>
`, this.gameOverlay.appendChild(n);
}
if (this.gameOverlay.appendChild(e), e.appendChild(t), this.config.closeOnEscape) {
const n = (o) => {
o.key === "Escape" && (this.closeGame(), document.removeEventListener("keydown", n));
};
document.addEventListener("keydown", n);
}
const i = typeof this.config.container == "string" ? document.querySelector(this.config.container) : this.config.container;
i && i.appendChild(this.gameOverlay);
}
initializeGame() {
if (!this.gameOverlay)
return;
const e = this.gameOverlay.querySelector("#snake-easter-egg-container");
if (!e)
return;
this.pixiApp = new E({
width: this.config.width,
height: this.config.height,
backgroundColor: 0,
antialias: !0
}), e.appendChild(this.pixiApp.view);
const t = new J((i) => {
this.config.onHighScore(i);
});
this.game = new Y(this.pixiApp, t), this.game.start();
}
destroyGameOverlay() {
this.pixiApp && (this.pixiApp.destroy(!0), this.pixiApp = null), this.game && (this.game = null), this.gameOverlay && (this.gameOverlay.remove(), this.gameOverlay = null);
}
}
class X {
constructor(e, t) {
s(this, "sequence", []);
s(this, "timeWindow");
s(this, "caseSensitive");
s(this, "currentIndex", 0);
s(this, "lastKeyTime", 0);
s(this, "callback");
this.sequence = e.keys, this.timeWindow = e.timeWindow || 3e3, this.caseSensitive = e.caseSensitive || !1, this.callback = t, document.addEventListener("keydown", this.handleKeyDown.bind(this));
}
handleKeyDown(e) {
const t = Date.now(), i = this.sequence[this.currentIndex], n = this.caseSensitive ? e.code : e.code.toLowerCase(), o = this.caseSensitive ? i : i.toLowerCase();
t - this.lastKeyTime > this.timeWindow && (this.currentIndex = 0), n === o ? (this.currentIndex++, this.lastKeyTime = t, this.currentIndex >= this.sequence.length && (this.callback(), this.currentIndex = 0)) : this.currentIndex = 0;
}
destroy() {
document.removeEventListener("keydown", this.handleKeyDown.bind(this));
}
}
class J {
constructor(e) {
s(this, "onHighScore");
this.onHighScore = e;
}
updateScore(e) {
y.updateHighScore(e) && this.onHighScore(e);
}
updateLevel() {
}
updateTheme() {
}
updateSkin() {
}
updateAchievements() {
}
updateGameStatus() {
}
unlockAchievement() {
}
gameCompleted(e) {
y.recordGame(e);
}
}
export {
j as SnakeGameEasterEgg
};