summaryrefslogtreecommitdiff
path: root/2026/games/ellg
diff options
context:
space:
mode:
Diffstat (limited to '2026/games/ellg')
-rw-r--r--2026/games/ellg/assets/beef1.pngbin0 -> 1245 bytes
-rw-r--r--2026/games/ellg/assets/beef2.pngbin0 -> 1280 bytes
-rw-r--r--2026/games/ellg/assets/chefhat.pngbin0 -> 1224 bytes
-rw-r--r--2026/games/ellg/assets/guy1.pngbin0 -> 1179 bytes
-rw-r--r--2026/games/ellg/assets/guy2.pngbin0 -> 1186 bytes
-rw-r--r--2026/games/ellg/assets/guy3.pngbin0 -> 1212 bytes
-rw-r--r--2026/games/ellg/assets/guy4.pngbin0 -> 1319 bytes
-rw-r--r--2026/games/ellg/assets/hungry.pngbin0 -> 1450 bytes
-rw-r--r--2026/games/ellg/assets/lose.pngbin0 -> 1629 bytes
-rw-r--r--2026/games/ellg/assets/pot.pngbin0 -> 1074 bytes
-rw-r--r--2026/games/ellg/assets/song.mp3bin0 -> 1196325 bytes
-rw-r--r--2026/games/ellg/assets/table.pngbin0 -> 276 bytes
-rw-r--r--2026/games/ellg/assets/win.pngbin0 -> 2357 bytes
-rw-r--r--2026/games/ellg/game.js378
-rw-r--r--2026/games/ellg/index.html38
-rw-r--r--2026/games/ellg/vector.js64
16 files changed, 480 insertions, 0 deletions
diff --git a/2026/games/ellg/assets/beef1.png b/2026/games/ellg/assets/beef1.png
new file mode 100644
index 0000000..0dbc9d9
--- /dev/null
+++ b/2026/games/ellg/assets/beef1.png
Binary files differ
diff --git a/2026/games/ellg/assets/beef2.png b/2026/games/ellg/assets/beef2.png
new file mode 100644
index 0000000..d2bd0c0
--- /dev/null
+++ b/2026/games/ellg/assets/beef2.png
Binary files differ
diff --git a/2026/games/ellg/assets/chefhat.png b/2026/games/ellg/assets/chefhat.png
new file mode 100644
index 0000000..951cac4
--- /dev/null
+++ b/2026/games/ellg/assets/chefhat.png
Binary files differ
diff --git a/2026/games/ellg/assets/guy1.png b/2026/games/ellg/assets/guy1.png
new file mode 100644
index 0000000..7b9b503
--- /dev/null
+++ b/2026/games/ellg/assets/guy1.png
Binary files differ
diff --git a/2026/games/ellg/assets/guy2.png b/2026/games/ellg/assets/guy2.png
new file mode 100644
index 0000000..fdecfe7
--- /dev/null
+++ b/2026/games/ellg/assets/guy2.png
Binary files differ
diff --git a/2026/games/ellg/assets/guy3.png b/2026/games/ellg/assets/guy3.png
new file mode 100644
index 0000000..f9a8ad2
--- /dev/null
+++ b/2026/games/ellg/assets/guy3.png
Binary files differ
diff --git a/2026/games/ellg/assets/guy4.png b/2026/games/ellg/assets/guy4.png
new file mode 100644
index 0000000..a3074a5
--- /dev/null
+++ b/2026/games/ellg/assets/guy4.png
Binary files differ
diff --git a/2026/games/ellg/assets/hungry.png b/2026/games/ellg/assets/hungry.png
new file mode 100644
index 0000000..1acc9dc
--- /dev/null
+++ b/2026/games/ellg/assets/hungry.png
Binary files differ
diff --git a/2026/games/ellg/assets/lose.png b/2026/games/ellg/assets/lose.png
new file mode 100644
index 0000000..7a1b38c
--- /dev/null
+++ b/2026/games/ellg/assets/lose.png
Binary files differ
diff --git a/2026/games/ellg/assets/pot.png b/2026/games/ellg/assets/pot.png
new file mode 100644
index 0000000..75e0ad9
--- /dev/null
+++ b/2026/games/ellg/assets/pot.png
Binary files differ
diff --git a/2026/games/ellg/assets/song.mp3 b/2026/games/ellg/assets/song.mp3
new file mode 100644
index 0000000..1437c97
--- /dev/null
+++ b/2026/games/ellg/assets/song.mp3
Binary files differ
diff --git a/2026/games/ellg/assets/table.png b/2026/games/ellg/assets/table.png
new file mode 100644
index 0000000..9ec1aab
--- /dev/null
+++ b/2026/games/ellg/assets/table.png
Binary files differ
diff --git a/2026/games/ellg/assets/win.png b/2026/games/ellg/assets/win.png
new file mode 100644
index 0000000..f47a046
--- /dev/null
+++ b/2026/games/ellg/assets/win.png
Binary files differ
diff --git a/2026/games/ellg/game.js b/2026/games/ellg/game.js
new file mode 100644
index 0000000..a82d24b
--- /dev/null
+++ b/2026/games/ellg/game.js
@@ -0,0 +1,378 @@
+import { Vector } from "./vector.js";
+
+const WIDTH = 240;
+const HEIGHT = 160;
+const STANDALONE = window.self === window.top;
+const DIFFICULTY = 4;
+
+const canvas = document.getElementById("game");
+
+const ctx = canvas.getContext("2d");
+ctx.imageSmoothingEnabled = false;
+
+function notifyHarness(msg) {
+ if (!STANDALONE) window.parent.postMessage(msg);
+}
+
+const music = new Audio("./assets/song.mp3");
+music.loop = true;
+music.volume = 0.5;
+
+function playMusic() {
+ music.currentTime = 0;
+ music.play().catch(() => { });
+}
+
+function stopMusic() {
+ music.pause();
+}
+
+export async function loadImage(url) {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => resolve(img);
+ img.onerror = reject;
+ img.src = url;
+ });
+};
+
+export async function loadAssets() {
+ const [guy1, guy2, guy3, guy4, pot, table, beef1, beef2, hat, win, lose, hungry] = await Promise.all([
+ loadImage("./assets/guy1.png"),
+ loadImage("./assets/guy2.png"),
+ loadImage("./assets/guy3.png"),
+ loadImage("./assets/guy4.png"),
+ loadImage("./assets/pot.png"),
+ loadImage("./assets/table.png"),
+ loadImage("./assets/beef1.png"),
+ loadImage("./assets/beef2.png"),
+ loadImage("./assets/chefhat.png"),
+ loadImage("./assets/win.png"),
+ loadImage("./assets/lose.png"),
+ loadImage("./assets/hungry.png"),
+ ]);
+
+ return {
+ guy1,
+ guy2,
+ guy3,
+ guy4,
+ pot,
+ table,
+ beef1,
+ beef2,
+ hat,
+ win,
+ lose,
+ hungry
+ };
+};
+
+let lastTime = 0;
+let assets = {};
+let state = {
+ scene: null,
+};
+
+class Scene {
+ constructor() { }
+
+ draw(dt) { }
+ update(dt) { }
+ handleInput(dt) { }
+ onClick(p) { }
+}
+
+const END_SCREEN_TIME = 3;
+
+class EndScreen extends Scene {
+ constructor(win) {
+ super();
+
+ stopMusic();
+
+ this.win = win;
+ this.t = 0;
+ this.sent = false;
+ }
+
+ update(dt) {
+ this.t += dt;
+
+ if (!this.sent && this.t >= END_SCREEN_TIME) {
+ this.sent = true;
+ notifyHarness({ op: "done", win: this.win });
+ }
+ }
+}
+
+class WinScreen extends EndScreen {
+ constructor() { super(true) }
+
+ draw() {
+ ctx.drawImage(assets.win, 0, 0);
+ }
+}
+
+class GameOverScreen extends EndScreen {
+ constructor(endType = "death") {
+ super(false);
+ this.endType = endType;
+ }
+
+ draw() {
+ ctx.drawImage(this.endType === "death" ? assets.lose : assets.hungry, 0, 0);
+ ctx.drawImage(assets.guy4, 50, 75);
+ }
+}
+
+const POT = { x: 25, y: 90, size: 64 };
+const POT_CENTER = Vector.of([POT.x + POT.size / 2, POT.y + POT.size / 2]);
+const COLLECT_TIME = 0.4;
+
+const BOUNDS = { x: 8, y: 8, w: 125, h: 96 };
+
+class Icon {
+ constructor(img, pos, correct) {
+ this.img = img;
+ this.pos = pos;
+ this.size = 32;
+ this.correct = correct;
+ this.vel = new Vector(0, 0);
+ this.collect = null;
+ this.dead = false;
+ }
+
+ static spawn(img, correct, speed) {
+ const icon = new Icon(img, Vector.of([0, 0]), correct);
+ icon.pos = Vector.of([
+ BOUNDS.x + Math.random() * (BOUNDS.w - icon.size),
+ BOUNDS.y + Math.random() * (BOUNDS.h - icon.size),
+ ]);
+ const a = Math.random() * Math.PI * 2;
+ icon.vel = new Vector(Math.cos(a), Math.sin(a)).multiply(speed);
+ return icon;
+ }
+
+ center() { return this.pos.add(new Vector(this.size / 2, this.size / 2)); }
+
+ contains(p) {
+ return p.x >= this.pos.x && p.x <= this.pos.x + this.size &&
+ p.y >= this.pos.y && p.y <= this.pos.y + this.size;
+ }
+
+ startCollect() {
+ if (this.collect) return;
+ this.collect = { t: 0, fromCenter: this.center() };
+ }
+
+ update(dt) {
+ if (this.collect) {
+ this.collect.t += dt;
+ const k = Math.min(1, this.collect.t / COLLECT_TIME);
+ const e = k * k;
+ this._drawCenter = this.collect.fromCenter.add(
+ POT_CENTER.subtract(this.collect.fromCenter).multiply(e));
+ this._scale = 1 - k;
+ if (k >= 1) this.dead = true;
+ return;
+ }
+
+ this.pos = this.pos.add(this.vel.multiply(dt));
+ const maxX = BOUNDS.x + BOUNDS.w - this.size;
+ const maxY = BOUNDS.y + BOUNDS.h - this.size;
+ if (this.pos.x < BOUNDS.x) { this.pos.x = BOUNDS.x; this.vel.x *= -1; }
+ if (this.pos.x > maxX) { this.pos.x = maxX; this.vel.x *= -1; }
+ if (this.pos.y < BOUNDS.y) { this.pos.y = BOUNDS.y; this.vel.y *= -1; }
+ if (this.pos.y > maxY) { this.pos.y = maxY; this.vel.y *= -1; }
+ }
+
+ draw(ctx) {
+ const c = this._drawCenter ?? this.center();
+ const s = this.size * (this._scale ?? 1);
+ ctx.drawImage(
+ this.img,
+ Math.round(c.x - s / 2), Math.round(c.y - s / 2),
+ Math.round(s), Math.round(s),
+ );
+ }
+}
+
+function difficultyConfig(raw) {
+ const K = 5;
+ const d = raw / (raw + K);
+ const lerp = (a, b) => a + (b - a) * d;
+ return {
+ correct: Math.round(lerp(3, 5)),
+ decoys: Math.round(lerp(3, 10)),
+ speed: lerp(20, 90),
+ timeLimit: lerp(8, 4),
+ };
+}
+
+function spawnIcons(cfg) {
+ const icons = [];
+ for (let i = 0; i < cfg.correct; i++) icons.push(Icon.spawn(assets.beef1, true, cfg.speed));
+ for (let i = 0; i < cfg.decoys; i++) icons.push(Icon.spawn(assets.beef2, false, cfg.speed));
+ return icons;
+}
+
+class GameScreen extends Scene {
+ constructor(cfg) {
+ super();
+ this.cfg = cfg;
+ this.timeTotal = cfg.timeLimit;
+ this.timeLeft = cfg.timeLimit;
+ this.state = {
+ guy: {
+ img: assets.guy2,
+ pos: Vector.of([125, 50]),
+ reactImg: null,
+ reactT: 0,
+ },
+ icons: spawnIcons(cfg),
+ };
+ }
+
+ draw() {
+ const guy = this.state.guy;
+
+ ctx.drawImage(guy.reactImg ?? guy.img, guy.pos.x, guy.pos.y);
+ ctx.drawImage(assets.hat, guy.pos.x + 45, guy.pos.y - 25, 50, 50);
+ ctx.drawImage(assets.table, 0, 50);
+
+ for (const icon of this.state.icons) icon.draw(ctx);
+
+ ctx.drawImage(assets.pot, POT.x, POT.y, POT.size, POT.size);
+
+ const frac = this.timeLeft / this.timeTotal;
+ ctx.fillStyle = "#3a2a1a";
+ ctx.fillRect(0, 0, WIDTH, 6);
+ ctx.fillStyle = frac < 0.25 ? "#e44" : "#f2c14e";
+ ctx.fillRect(0, 0, Math.round(WIDTH * frac), 6);
+ }
+
+ update(dt, ct) {
+ if (this.state.guy.reactT > 0) {
+ this.state.guy.reactT -= dt;
+ if (this.state.guy.reactT <= 0) this.state.guy.reactImg = null;
+ }
+
+ this.timeLeft -= dt;
+ if (this.timeLeft <= 0) {
+ this.timeLeft = 0;
+ this.lose("hungry");
+ return;
+ }
+
+ const baseY = 50;
+ const amp = 4;
+ const speed = 0.01;
+ this.state.guy.pos.y = baseY + Math.round(Math.sin(ct * speed) * amp);
+
+ for (const icon of this.state.icons) icon.update(dt);
+ this.state.icons = this.state.icons.filter(i => !i.dead);
+ }
+
+ lose(endType = "death") {
+ state.scene = new GameOverScreen(endType);
+ }
+
+ win() {
+ state.scene = new WinScreen();
+ }
+
+ onClick(p) {
+ for (let i = this.state.icons.length - 1; i >= 0; i--) {
+ const icon = this.state.icons[i];
+ if (icon.collect) continue;
+ if (icon.contains(p)) {
+ if (!icon.correct) { this.lose() }
+
+ icon.startCollect();
+
+ const correctLeft = this.state.icons.filter(x => x.correct && !x.collect).length;
+ if (correctLeft === 0) this.win()
+
+ if (icon.correct) {
+ this.state.guy.reactImg = assets.guy3;
+ this.state.guy.reactT = 2;
+ };
+
+ return;
+ }
+ }
+ }
+}
+
+async function init() {
+ assets = await loadAssets();
+ notifyHarness({ op: "ready" });
+}
+
+function update(dt, ct) {
+ if (state.scene) {
+ state.scene.update(dt, Math.floor(ct));
+ }
+}
+
+function draw(dt) {
+ ctx.fillStyle = "#222";
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ if (state.scene) {
+ state.scene.draw(dt);
+ }
+}
+
+function handleInput(dt) {
+ if (state.scene) {
+ state.scene.handleInput(dt);
+ }
+}
+
+function main(currentTime) {
+ if (!lastTime) lastTime = currentTime;
+ const dt = Math.min((currentTime - lastTime) / 1000, 0.1);
+
+ lastTime = currentTime;
+
+ handleInput(dt);
+ update(dt, currentTime);
+ draw(dt);
+
+ requestAnimationFrame(main);
+}
+
+function startGame(difficulty = DIFFICULTY) {
+ state.scene = new GameScreen(difficultyConfig(difficulty));
+ notifyHarness({ op: "started", verb: "make some chili!" });
+ playMusic();
+}
+
+canvas.addEventListener("click", (ev) => {
+ if (music.paused && state.scene instanceof GameScreen) music.play().catch(() => { });
+
+ const r = canvas.getBoundingClientRect();
+ const p = Vector.of([
+ (ev.clientX - r.left) / r.width * WIDTH,
+ (ev.clientY - r.top) / r.height * HEIGHT,
+ ]);
+ state.scene?.onClick(p);
+});
+
+window.addEventListener("message", ev => {
+ switch (ev.data.op) {
+ case "start":
+ startGame(ev.data.difficulty ?? 0);
+ break;
+ default: console.log(`unknown event: ${ev}`); break;
+ }
+});
+
+await init();
+
+requestAnimationFrame(main);
+
+if (STANDALONE) startGame();
+
diff --git a/2026/games/ellg/index.html b/2026/games/ellg/index.html
new file mode 100644
index 0000000..a845dea
--- /dev/null
+++ b/2026/games/ellg/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+ <title>watch the pot</title>
+ <style>
+ * {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ html,body {
+ width: 100%;
+ height: 100%;
+ }
+
+ body {
+ font-size: 10px;
+ background: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ }
+
+ #game {
+ width: min(100vw, 150vh);
+ aspect-ratio: 3 / 2;
+ image-rendering: pixelated;
+ }
+ </style>
+ </head>
+ <body>
+ <canvas id="game" width="240" height="160" />
+ <script type="module" src="./game.js"></script>
+ </body>
+</html>
diff --git a/2026/games/ellg/vector.js b/2026/games/ellg/vector.js
new file mode 100644
index 0000000..cf32d81
--- /dev/null
+++ b/2026/games/ellg/vector.js
@@ -0,0 +1,64 @@
+export class Vector {
+ static of([x, y]) {
+ return new Vector(x, y)
+ }
+
+ constructor(x, y) {
+ this.x = x
+ this.y = y
+ }
+
+ add(val) {
+ return new Vector(this.x + val.x, this.y + val.y)
+ }
+
+ subtract(val) {
+ return new Vector(this.x - val.x, this.y - val.y)
+ }
+
+ multiply(scalar) {
+ return new Vector(this.x * scalar, this.y * scalar)
+ }
+
+ divide(scalar) {
+ return new Vector(this.x / scalar, this.y / scalar)
+ }
+
+ dot(other) {
+ return this.x * other.x + this.y * other.y
+ }
+
+ cross(other) {
+ return this.x * other.y - other.x * this.y
+ }
+
+ hadamard(other) {
+ return new Vector(this.x * other.x, this.y * other.y)
+ }
+
+ length() {
+ return Math.sqrt(this.x ** 2 + this.y ** 2)
+ }
+
+ distance(other) {
+ return this.subtract(other).length()
+ }
+
+ normalize() {
+ const length = this.length()
+ if (length === 0) {
+ return new Vector(0, 0)
+ }
+ return new Vector(this.x / length, this.y / length)
+ }
+
+ rotateByRadians(radians) {
+ const cos = Math.cos(radians)
+ const sin = Math.sin(radians)
+ return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos)
+ }
+
+ rotateByDegrees(degrees) {
+ return this.rotateByRadians((degrees * Math.PI) / 180)
+ }
+}