Euler golf #1

Merged
Simponic merged 5 commits from euler-golf into main 2023-02-24 17:02:20 -05:00
9 changed files with 898 additions and 39 deletions

74
euler-golf/css/styles.css Normal file
View File

@ -0,0 +1,74 @@
body {
margin: 0;
padding: 0;
overflow: scroll;
font-family: Lucida Console, Lucida Sans Typewriter, monaco,
Bitstream Vera Sans Mono, monospace;
width: 100vw;
height: 100vh;
background: rgb(238, 174, 202);
background: radial-gradient(
circle,
rgba(238, 174, 202, 1) 0%,
rgba(148, 187, 233, 1) 100%
);
}
.canvas {
padding: 0;
margin: auto;
display: block;
border: 1px solid black;
width: 100vw;
height: 100vw;
}
button {
border-radius: 5px;
padding: 5px;
cursor: pointer;
margin-left: 5px;
}
.controls {
cursor: pointer;
padding: 12px;
position: fixed;
bottom: 0;
right: 0;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid white;
border-radius: 8px;
margin-right: 6px;
margin-bottom: 6px;
}
.buttons {
display: flex;
justify-content: space-around;
align-items: center;
}
.modal {
display: flex;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 80vw;
max-width: 500px;
min-height: 200px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid black;
border-radius: 15px;
}
.modal-body {
display: flex;
justify-content: center;
}

74
euler-golf/index.html Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<title>Euler Golf 2</title>
<link rel="stylesheet" type="text/css" href="css/styles.css" />
</head>
<body>
<canvas id="canvas"></canvas>
<div class="controls" id="controls-container">
<div id="controls" style="display: none">
<div class="buttons">
<button id="reset">Reset</button>
<button id="solve">Solve</button>
<button id="directions">Directions</button>
</div>
</div>
<span id="expand-show">↑↑</span>
</div>
<div
id="directions-modal"
class="modal"
style="display: none"
tabindex="-1"
role="dialog"
>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">X</span>
</button>
<div class="modal-body">
<div style="margin: 0; display: inline-block">
<h1 style="text-align: center">Euler Golf 2</h1>
<p>
Use the left and right arrow keys as navigation & hover over the
bottom right corner for controls.
</p>
<p>Rules</p>
<ul>
<li>
Every move consists of a 90 degree rotation around your last
position.
</li>
<li>You begin at the point one unit right from the center.</li>
<li>
The inital point that you rotate around is the origin (blue).
</li>
<li>You must navigate to the target point (green).</li>
</ul>
<p>
Initial game by
<a href="https://kylehovey.github.io/EulerGolf/">speleo</a>,
reimplemented & solved by
<a href="https://github.com/Simponic">simponic</a>.
</p>
</div>
</div>
</div>
<script src="js/modal-vanilla.min.js"></script>
<script src="js/cx.js"></script>
<script src="js/json-ds.js"></script>
<script src="js/sol.js"></script>
<script src="js/game.js"></script>
<script src="js/controls.js"></script>
</body>
</html>

32
euler-golf/js/controls.js vendored Normal file
View File

@ -0,0 +1,32 @@
const directions_modal = new Modal({
el: document.getElementById("directions-modal"),
});
document
.getElementById("controls-container")
.addEventListener("mouseover", () => {
document.getElementById("controls").style.display = "block";
document.getElementById("expand-show").style.display = "none";
});
document
.getElementById("controls-container")
.addEventListener("mouseout", () => {
document.getElementById("controls").style.display = "none";
document.getElementById("expand-show").style.display = "inline";
});
document.getElementById("reset").addEventListener("click", () => {
state = reset_state(state);
state.target = rand_target(state.rows, state.cols);
});
document.getElementById("solve").addEventListener("click", () => {
if (!cx.eq(state.path.at(-2), new cx(0, 0))) state = reset_state(state);
state.solution = sol(state.target);
});
document
.getElementById("directions")
.addEventListener("click", () => directions_modal.show());

308
euler-golf/js/cx.js Normal file
View File

@ -0,0 +1,308 @@
// http://www.russellcottrell.com/fractalsEtc/cx.js
class cx {
static degrees(d) {
cx._RD = d ? Math.PI / 180 : 1;
}
// Math.PI/180 for degrees, 1 for radians
// applies to i/o (constructor, get/set arg, and toString etc.)
constructor(x, y, polar) {
if (!polar) {
this.re = x;
this.im = y;
} else {
y *= cx._RD; // may be radians or degrees
this.re = x * Math.cos(y);
this.im = x * Math.sin(y);
}
}
get abs() {
return Math.sqrt(this.re * this.re + this.im * this.im);
}
set abs(r) {
var theta = this._arg;
this.re = r * Math.cos(theta);
this.im = r * Math.sin(theta);
}
get arg() {
// returns radians or degrees, non-negative
return (
((Math.atan2(this.im, this.re) + 2 * Math.PI) % (2 * Math.PI)) / cx._RD
);
}
set arg(theta) {
// may be radians or degrees
var r = this.abs;
this.re = r * Math.cos(theta * cx._RD);
this.im = r * Math.sin(theta * cx._RD);
}
get _arg() {
// internal; returns radians
return Math.atan2(this.im, this.re);
}
static get i() {
return new cx(0, 1);
}
static set i(x) {
throw new Error("i is read-only");
}
toString(polar) {
if (!polar)
return (
this.re.toString() +
(this.im >= 0 ? " + " : " - ") +
Math.abs(this.im).toString() +
"i"
);
else return this.abs.toString() + " cis " + this.arg.toString();
}
toPrecision(n, polar) {
if (!polar)
return (
this.re.toPrecision(n) +
(this.im >= 0 ? " + " : " - ") +
Math.abs(this.im).toPrecision(n) +
"i"
);
else return this.abs.toPrecision(n) + " cis " + this.arg.toPrecision(n);
}
toPrecis(n, polar) {
// trims trailing zeros
if (!polar)
return (
parseFloat(this.re.toPrecision(n)).toString() +
(this.im >= 0 ? " + " : " - ") +
parseFloat(Math.abs(this.im).toPrecision(n)).toString() +
"i"
);
else
return (
parseFloat(this.abs.toPrecision(n)).toString() +
" cis " +
parseFloat(this.arg.toPrecision(n)).toString()
);
}
toFixed(n, polar) {
if (!polar)
return (
this.re.toFixed(n) +
(this.im >= 0 ? " + " : " - ") +
Math.abs(this.im).toFixed(n) +
"i"
);
else return this.abs.toFixed(n) + " cis " + this.arg.toFixed(n);
}
toExponential(n, polar) {
if (!polar)
return (
this.re.toExponential(n) +
(this.im >= 0 ? " + " : " - ") +
Math.abs(this.im).toExponential(n) +
"i"
);
else return this.abs.toExponential(n) + " cis " + this.arg.toExponential(n);
}
static getReals(c, d) {
// when c or d may be simple or complex
var x, y, u, v;
if (c instanceof cx) {
x = c.re;
y = c.im;
} else {
x = c;
y = 0;
}
if (d instanceof cx) {
u = d.re;
v = d.im;
} else {
u = d;
v = 0;
}
return [x, y, u, v];
}
static conj(c) {
return new cx(c.re, -c.im);
}
static neg(c) {
return new cx(-c.re, -c.im);
}
static add(c, d) {
var a = cx.getReals(c, d);
var x = a[0];
var y = a[1];
var u = a[2];
var v = a[3];
return new cx(x + u, y + v);
}
static sub(c, d) {
var a = cx.getReals(c, d);
var x = a[0];
var y = a[1];
var u = a[2];
var v = a[3];
return new cx(x - u, y - v);
}
static mult(c, d) {
var a = cx.getReals(c, d);
var x = a[0];
var y = a[1];
var u = a[2];
var v = a[3];
return new cx(x * u - y * v, x * v + y * u);
}
static div(c, d) {
var a = cx.getReals(c, d);
var x = a[0];
var y = a[1];
var u = a[2];
var v = a[3];
return new cx(
(x * u + y * v) / (u * u + v * v),
(y * u - x * v) / (u * u + v * v)
);
}
static pow(c, int) {
if (Number.isInteger(int) && int >= 0) {
var r = Math.pow(c.abs, int);
var theta = int * c._arg;
return new cx(r * Math.cos(theta), r * Math.sin(theta));
} else return NaN;
}
static root(c, int, k) {
if (!k) k = 0;
if (
Number.isInteger(int) &&
int >= 2 &&
Number.isInteger(k) &&
k >= 0 &&
k < int
) {
var r = Math.pow(c.abs, 1 / int);
var theta = (c._arg + 2 * k * Math.PI) / int;
return new cx(r * Math.cos(theta), r * Math.sin(theta));
} else return NaN;
}
static log(c) {
return new cx(Math.log(c.abs), c._arg);
}
static exp(c) {
var r = Math.exp(c.re);
var theta = c.im;
return new cx(r * Math.cos(theta), r * Math.sin(theta));
}
static sin(c) {
var a = c.re;
var b = c.im;
return new cx(Math.sin(a) * Math.cosh(b), Math.cos(a) * Math.sinh(b));
}
static cos(c) {
var a = c.re;
var b = c.im;
return new cx(Math.cos(a) * Math.cosh(b), -Math.sin(a) * Math.sinh(b));
}
static tan(c) {
return cx.div(cx.sin(c), cx.cos(c));
}
static asin(c, k) {
if (!k) k = 0;
var ic = cx.mult(cx.i, c);
var c2 = cx.pow(c, 2);
return cx.mult(
cx.neg(cx.i),
cx.log(cx.add(ic, cx.root(cx.sub(1, c2), 2, k)))
);
}
static acos(c, k) {
if (!k) k = 0;
var c2 = cx.pow(c, 2);
return cx.mult(
cx.neg(cx.i),
cx.log(cx.add(c, cx.mult(cx.i, cx.root(cx.sub(1, c2), 2, k))))
);
}
static atan(c) {
return cx.mult(
cx.div(cx.i, 2),
cx.log(cx.div(cx.add(cx.i, c), cx.sub(cx.i, c)))
);
}
static sinh(c) {
var a = c.re;
var b = c.im;
return new cx(Math.sinh(a) * Math.cos(b), Math.cosh(a) * Math.sin(b));
}
static cosh(c) {
var a = c.re;
var b = c.im;
return new cx(Math.cosh(a) * Math.cos(b), Math.sinh(a) * Math.sin(b));
}
static tanh(c) {
return cx.div(cx.sinh(c), cx.cosh(c));
}
static asinh(c, k) {
if (!k) k = 0;
var c2 = cx.pow(c, 2);
return cx.log(cx.add(c, cx.root(cx.add(c2, 1), 2, k)));
}
static acosh(c, k) {
if (!k) k = 0;
var c2 = cx.pow(c, 2);
return cx.log(cx.add(c, cx.root(cx.sub(c2, 1), 2, k)));
}
static atanh(c) {
return cx.mult(cx.div(1, 2), cx.log(cx.div(cx.add(1, c), cx.sub(1, c))));
}
static copy(c) {
return new cx(c.re, c.im);
}
static eq(c, d, epsilon) {
if (!epsilon) {
if (c.re == d.re && c.im == d.im) return true;
} else {
if (Math.abs(c.re - d.re) < epsilon && Math.abs(c.im - d.im) < epsilon)
return true;
}
return false;
}
}
cx.degrees(true); // need to call this

276
euler-golf/js/game.js Normal file
View File

@ -0,0 +1,276 @@
const DEFAULTS = {
max_rows: 80,
max_cols: 80,
min_gap: 30,
angle_multiplier: 10e-4,
};
const CANVAS = document.getElementById("canvas");
let state = {
grid_padding: 30,
canvas: CANVAS,
ctx: CANVAS.getContext("2d"),
last_render: 0,
keys: {},
changes: {},
};
// Rendering
CanvasRenderingContext2D.prototype.circle = function (x, y, r, color) {
this.beginPath();
this.arc(x, y, r, 0, Math.PI * 2);
this.fillStyle = color;
this.fill();
this.closePath();
};
CanvasRenderingContext2D.prototype.line = function (
{ x_pos: x1, y_pos: y1 },
{ x_pos: x2, y_pos: y2 },
width,
color,
cap = "round"
) {
this.lineWidth = width;
this.strokeStyle = color;
this.lineCap = cap;
this.beginPath();
this.moveTo(x1, y1);
this.lineTo(x2, y2);
this.stroke();
this.closePath();
};
CanvasRenderingContext2D.prototype.draw_cartesian_path = function (
grid_spec,
cartesian_path,
width = 2,
color = "#000"
) {
const path = cartesian_path.map((coord) => grid_to_canvas(coord, grid_spec));
path.slice(1).forEach((coord, i) => {
this.line(path[i], coord, width, color);
});
};
CanvasRenderingContext2D.prototype.do_grid = function (
rows,
cols,
draw_at_grid_pos = (ctx, x, y) => ctx.circle(x, y, 10, "#44ff44")
) {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
draw_at_grid_pos(this, x, y);
}
}
};
CanvasRenderingContext2D.prototype.cartesian_grid = function (
rows,
cols,
grid_spec,
circle_spec_at_coords = (_x, _y) => ({ radius: 5, color: "#000" })
) {
this.do_grid(rows, cols, (ctx, x, y) => {
const { x_pos, y_pos } = grid_to_canvas({ x, y }, grid_spec);
const { radius, color } = circle_spec_at_coords(x, y);
ctx.circle(x_pos, y_pos, radius, color);
});
};
// Utilities
const move = (prev, curr, c) => cx.add(prev, cx.mult(c, cx.sub(curr, prev)));
const rand_between = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const rand_target = (rows, cols) => {
const r = Math.floor(rows / 2);
const c = Math.floor(cols / 2);
const res = new cx(rand_between(-c, c), rand_between(-r, r));
if (!sol(res)) return rand_target(rows, cols);
return res;
};
const calculate_grid_spec = ({ rows, cols, width, height, grid_padding }) => {
const dx = (width - 2 * grid_padding) / cols;
const dy = (height - 2 * grid_padding) / rows;
return {
dx,
dy,
start_x: grid_padding + dx / 2,
start_y: grid_padding + dy / 2,
};
};
const grid_to_canvas = ({ x, y }, { dx, dy, start_x, start_y }) => ({
x_pos: x * dx + start_x,
y_pos: y * dy + start_y,
});
const complex_to_grid = (c, rows, cols) => {
const { re, im } = c;
return {
x: re + Math.floor(cols / 2),
y: Math.floor(rows / 2) - im,
};
};
// Game loop
const maybe_add_state_angle_move = ({ angle } = state) => {
if (angle.im <= -1 || angle.im >= 1) {
angle.im = angle.im <= -1 ? -1 : 1;
state.path.push(move(state.path.at(-2), state.path.at(-1), angle));
state.angle = new cx(0, 0);
}
return state;
};
const handle_input = (state, dt) => {
if (state.keys.ArrowLeft) {
state.angle.im += DEFAULTS.angle_multiplier * dt;
} else if (state.keys.ArrowRight) {
state.angle.im -= DEFAULTS.angle_multiplier * dt;
}
state = maybe_add_state_angle_move(state);
};
const render = ({ width, height, ctx, rows, cols, target } = state) => {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "rgba(0, 0, 0, 0)";
ctx.fillRect(0, 0, width, height);
const grid_spec = calculate_grid_spec(state);
const curr = state.path.at(-1);
const prev = state.path.at(-2);
const v_diff = cx.sub(curr, prev);
const theta = (state.angle.im * Math.PI) / 2;
const angle_re = Math.cos(theta) * v_diff.re - Math.sin(theta) * v_diff.im;
const angle_im = Math.sin(theta) * v_diff.re + Math.cos(theta) * v_diff.im;
ctx.draw_cartesian_path(grid_spec, [
...state.path.map((c) => complex_to_grid(c, rows, cols)),
complex_to_grid(cx.add(new cx(angle_re, angle_im), prev), rows, cols),
]);
if (!(state.angle.im == state.angle.re && state.angle.re == 0)) {
// Draw path to next player's target
const [a, b] = [
curr,
move(prev, curr, new cx(0, state.angle.im < 0 ? -1 : 1)),
].map((c) => grid_to_canvas(complex_to_grid(c, rows, cols), grid_spec));
ctx.line(a, b, 6, "#aaa");
}
const grid_target = complex_to_grid(target, rows, cols);
ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => {
if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) {
return {
radius: 7,
color: "#2f9c94",
};
} else if (x == grid_target.x && y == grid_target.y) {
return {
radius: 8,
color: "#00ff00",
};
} else {
return {
radius: 3,
color: `rgb(${255 * (x / cols)}, 100, 100)`, // todo: animate with last_render
};
}
});
};
const loop = (now) => {
const dt = now - state.last_render;
state.changes.last_render = now;
if (Object.keys(state.changes).length > 0) {
if (state.changes.width || state.changes.height) {
state.changes.rows = Math.floor(
Math.min(DEFAULTS.max_rows, state.changes.height / DEFAULTS.min_gap)
);
state.changes.cols = Math.floor(
Math.min(DEFAULTS.max_cols, state.changes.width / DEFAULTS.min_gap)
);
}
state = { ...state, ...state.changes };
state.changes = {};
}
if (!state.target) {
state.target = rand_target(state.rows, state.cols);
}
if (!state.solution) {
handle_input(state, dt);
} else {
if (!state?.solution.length) {
delete state.solution;
} else {
state.angle.im +=
(state.solution[0] === "-" ? 1 : -1) * DEFAULTS.angle_multiplier * dt;
state = maybe_add_state_angle_move(state);
if (cx.eq(state.angle, new cx(0, 0))) state.solution.shift();
}
}
render(state);
requestAnimationFrame(loop);
};
const reset_state = ({ rows, cols } = state) => ({
...state,
solution: null,
path: [new cx(0, 0), new cx(1, 0)],
angle: new cx(0, 0),
});
// DOM
const on_resize = () => {
CANVAS.width = document.body.clientWidth;
CANVAS.height = document.body.clientHeight;
state.changes.width = CANVAS.width;
state.changes.height = CANVAS.height;
};
const on_keyup = (e) => {
delete state.keys[e.key];
};
const on_keydown = (e) => {
state.keys[e.key] = true;
};
window.addEventListener("resize", on_resize);
window.addEventListener("keydown", on_keydown);
window.addEventListener("keyup", on_keyup);
// main
on_resize();
state = reset_state(state);
if (!sessionStorage.getItem("seen-instructions")) {
new Modal({
el: document.getElementById("directions-modal"),
}).show();
sessionStorage.setItem("seen-instructions", true);
}
requestAnimationFrame(loop);

19
euler-golf/js/json-ds.js Normal file
View File

@ -0,0 +1,19 @@
class JSONSet {
items = new Set();
constructor(initial) {
if (Array.isArray(initial)) {
initial.map((x) => this.apply_set_function("add", x));
} else {
this.apply_set_function("add", initial);
}
["add", "has", "remove"].forEach(
(f_name) => (this[f_name] = (x) => this.apply_set_function(f_name, x))
);
}
apply_set_function(f_name, x) {
return this.items[f_name](JSON.stringify(x));
}
}

1
euler-golf/js/modal-vanilla.min.js vendored Normal file

File diff suppressed because one or more lines are too long

39
euler-golf/js/sol.js Normal file
View File

@ -0,0 +1,39 @@
const DEPTH = 15;
const DIRECTION = {
0: new cx(0, 1),
1: new cx(0, -1),
};
const construct_moves = (curr, prev) =>
Object.keys(DIRECTION).map((x) => move(curr, prev, DIRECTION[x]));
const backtrack = (local_index, depth) =>
local_index
.toString(2)
.padStart(depth, "0")
.split("")
.map((direction) => (Number(direction) ? "+" : "-"));
const sol = (target, start_from = new cx(0, 0), start_to = new cx(1, 0)) => {
let moves = [start_to, ...construct_moves(start_from, start_to)];
let curr_depth = 2;
while (curr_depth < DEPTH) {
for (let i = 0; i < Math.pow(2, curr_depth); i++) {
const direction = DIRECTION[Number(i.toString(2).at(-1))];
// Current element is at i >> 1 + the offset for the previous group (which is
// the sum of the geometric series 2**n until curr_depth - 1)
const current_i = (i >> 1) + (1 - Math.pow(2, curr_depth - 1)) / (1 - 2);
const previous_i = (i >> 2) + (1 - Math.pow(2, curr_depth - 2)) / (1 - 2);
const new_move = move(moves[previous_i], moves[current_i], direction);
moves.push(new_move);
if (cx.eq(new_move, target)) return backtrack(i, curr_depth);
}
curr_depth++;
}
return null;
};

View File

@ -3,56 +3,41 @@
<html>
<head>
<title>Simponic's Static Sites</title>
<link href="css/styles.css" rel="stylesheet">
<link href="css/styles.css" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<script src="https://kit.fontawesome.com/d7e97ed48f.js" crossorigin="anonymous"></script>
<script
src="https://kit.fontawesome.com/d7e97ed48f.js"
crossorigin="anonymous"
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div class="top-container animate__animated animate__fadeIn">
<img src="images/profile.png" class="profile-picture">
<img src="images/profile.png" class="profile-picture" />
<h3>
👋 Hello, I'm Simponic!
<br>
<br />
📖 This page hosts strictly static content.
<br>
🔔 My "real website" is at <a href="https://simponic.xyz">simponic.xyz</a>.
<br />
🔔 My "real website" is at
<a href="https://simponic.xyz">simponic.xyz</a>.
</h3>
<div class="projects-grid">
<div class="project" onclick="window.location='dvd-logo/index.html'">
<div class="project" onclick="window.location='euler-golf/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-compact-disc"></i>
<i class="fa-solid fa-golf-ball-tee"></i>
</div>
<div class="project-body">
<h1>DVD Logo Bouncing Animation</h1>
<p>Brings back the nostalgia of old-school DVD players with an intersection predictor. The twist: no Canvas API! Only svg's and absolute positioned images!</p>
</div>
</div>
<div class="project" onclick="window.location='maize-maze/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-diagram-project"></i>
</div>
<div class="project-body" >
<h1>The A-maze-ing Maize Maze</h1>
<p>A Randomized Kruskal's Maze game with BFS path-finding. You play as a 🌽corn stalk trying to become 🍿popcorn.</p>
</div>
</div>
<div class="project" onclick="window.location='centipede/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-mosquito"></i>
</div>
<div class="project-body" >
<h1>Centipede</h1>
<p>In this game, shoot all the centipede bodies and score points and go up levels.</p>
<h1>Euler Golf 2</h1>
<p>
A puzzle game (with solver) to explore rotations in the complex
plane.
</p>
</div>
</div>
@ -63,18 +48,69 @@
<div class="project-body">
<h1>Julia Set Explorer</h1>
<p>Zoom, pan, and "c" complex changes in this fun GPU-accelerated playground!</p>
<p>
Zoom, pan, and "c" complex changes in this fun GPU-accelerated
playground!
</p>
</div>
</div>
<div class="project" onclick="window.location='ft-visualizer/index.html'">
<div
class="project"
onclick="window.location='ft-visualizer/index.html'"
>
<div class="project-logo-container">
<i class="fa-solid fa-wave-square"></i>
</div>
<div class="project-body">
<h1>Discrete Fourier Visualizer</h1>
<p>Draw how your year has gone and view a reactive graph containing its DFT by dragging your mouse over the canvas!</p>
<p>
Draw how your year has gone and view a reactive graph containing
its DFT by dragging your mouse over the canvas!
</p>
</div>
</div>
<div class="project" onclick="window.location='maize-maze/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-diagram-project"></i>
</div>
<div class="project-body">
<h1>The A-maze-ing Maize Maze</h1>
<p>
A Randomized Kruskal's Maze game with BFS path-finding. You play
as a 🌽corn stalk trying to become 🍿popcorn.
</p>
</div>
</div>
<div class="project" onclick="window.location='centipede/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-mosquito"></i>
</div>
<div class="project-body">
<h1>Centipede</h1>
<p>
In this game, shoot all the centipede bodies and score points and
go up levels.
</p>
</div>
</div>
<div class="project" onclick="window.location='dvd-logo/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-compact-disc"></i>
</div>
<div class="project-body">
<h1>DVD Logo Bouncing Animation</h1>
<p>
Brings back the nostalgia of old-school DVD players with an
intersection predictor. The twist: no Canvas API! Only svg's and
absolute positioned images!
</p>
</div>
</div>
</div>