Euler golf #1

Merged
Simponic merged 5 commits from euler-golf into main 2023-02-24 17:02:20 -05:00
7 changed files with 215 additions and 42 deletions
Showing only changes of commit 8050a399df - Show all commits

View File

@ -24,21 +24,51 @@ body {
height: 100vw; height: 100vw;
} }
.button { button {
border: 1px solid black;
border-radius: 5px; border-radius: 5px;
padding: 5px; padding: 5px;
cursor: pointer; cursor: pointer;
margin-left: 5px;
} }
.controls { .controls {
cursor: pointer;
padding: 12px; padding: 12px;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 0; right: 0;
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.8);
border: 1px solid white; border: 1px solid white;
border-radius: 8px; border-radius: 8px;
margin-right: 6px; margin-right: 6px;
margin-bottom: 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;
}

View File

@ -7,13 +7,68 @@
<body> <body>
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div class="controls"> <div class="controls" id="controls-container">
<span id="iteration-count"></span> <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>
<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/cx.js"></script>
<script src="js/json-ds.js"></script> <script src="js/json-ds.js"></script>
<script src="js/sol.js"></script> <script src="js/sol.js"></script>
<script src="js/game.js"></script> <script src="js/game.js"></script>
<script src="js/controls.js"></script>
</body> </body>
</html> </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());

View File

@ -1,18 +1,16 @@
const DEFAULTS = { const DEFAULTS = {
max_rows: 80, max_rows: 80,
max_cols: 80, max_cols: 80,
min_gap: 40, min_gap: 30,
angle_multiplier: 10e-4, angle_multiplier: 10e-4,
}; };
const CANVAS = document.getElementById("canvas"); const CANVAS = document.getElementById("canvas");
let state = { let state = {
grid_padding: 10, grid_padding: 30,
canvas: CANVAS, canvas: CANVAS,
ctx: CANVAS.getContext("2d"), ctx: CANVAS.getContext("2d"),
last_render: 0, last_render: 0,
path: [new cx(0, 0), new cx(1, 0)],
angle: new cx(0, 0),
keys: {}, keys: {},
changes: {}, changes: {},
}; };
@ -59,7 +57,7 @@ CanvasRenderingContext2D.prototype.draw_cartesian_path = function (
CanvasRenderingContext2D.prototype.do_grid = function ( CanvasRenderingContext2D.prototype.do_grid = function (
rows, rows,
cols, cols,
draw_at_grid_pos = (ctx, x, y) => ctx.circle(x, y, 10, "#00ff00") draw_at_grid_pos = (ctx, x, y) => ctx.circle(x, y, 10, "#44ff44")
) { ) {
for (let y = 0; y < rows; y++) { for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) { for (let x = 0; x < cols; x++) {
@ -88,9 +86,11 @@ const move = (prev, curr, c) => cx.add(prev, cx.mult(c, cx.sub(curr, prev)));
const rand_between = (min, max) => const rand_between = (min, max) =>
Math.floor(Math.random() * (max - min + 1)) + min; Math.floor(Math.random() * (max - min + 1)) + min;
const rand_target = (cols, rows) => { const rand_target = (rows, cols) => {
const res = new cx(rand_between(0, cols), rand_between(0, rows)); const r = Math.floor(rows / 2);
if (res.re % 2 || res.im % 2) return rand_target(cols, rows); 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; return res;
}; };
@ -121,21 +121,26 @@ const complex_to_grid = (c, rows, cols) => {
}; };
// Game loop // 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) => { const handle_input = (state, dt) => {
if (state.keys.ArrowLeft) { if (state.keys.ArrowLeft) {
state.angle.im += DEFAULTS.angle_multiplier * dt; state.angle.im += DEFAULTS.angle_multiplier * dt;
} else if (state.keys.ArrowRight) { } else if (state.keys.ArrowRight) {
state.angle.im -= DEFAULTS.angle_multiplier * dt; state.angle.im -= DEFAULTS.angle_multiplier * dt;
} }
state = maybe_add_state_angle_move(state);
if (state.angle.im <= -1 || state.angle.im >= 1) {
state.angle.im = state.angle.im <= -1 ? -1 : 1;
state.path.push(move(state.path.at(-2), state.path.at(-1), state.angle));
state.angle = new cx(0, 0);
}
}; };
const render = ({ width, height, ctx, rows, cols } = state) => { const render = ({ width, height, ctx, rows, cols, target } = state) => {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "rgba(0, 0, 0, 0)"; ctx.fillStyle = "rgba(0, 0, 0, 0)";
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
@ -156,19 +161,17 @@ const render = ({ width, height, ctx, rows, cols } = state) => {
complex_to_grid(cx.add(new cx(angle_re, angle_im), prev), rows, cols), complex_to_grid(cx.add(new cx(angle_re, angle_im), prev), rows, cols),
]); ]);
ctx.line( if (!(state.angle.im == state.angle.re && state.angle.re == 0)) {
grid_to_canvas(complex_to_grid(curr, rows, cols), grid_spec), // Draw path to next player's target
grid_to_canvas( const [a, b] = [
complex_to_grid( curr,
move(prev, curr, new cx(0, state.angle.im < 0 ? -1 : 1)), move(prev, curr, new cx(0, state.angle.im < 0 ? -1 : 1)),
rows, ].map((c) => grid_to_canvas(complex_to_grid(c, rows, cols), grid_spec));
cols
), ctx.line(a, b, 6, "#aaa");
grid_spec }
),
6, const grid_target = complex_to_grid(target, rows, cols);
"#aaa"
);
ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => { ctx.cartesian_grid(rows, cols, grid_spec, (x, y) => {
if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) { if (x == Math.floor(cols / 2) && y == Math.floor(rows / 2)) {
@ -176,6 +179,11 @@ const render = ({ width, height, ctx, rows, cols } = state) => {
radius: 7, radius: 7,
color: "#2f9c94", color: "#2f9c94",
}; };
} else if (x == grid_target.x && y == grid_target.y) {
return {
radius: 8,
color: "#00ff00",
};
} else { } else {
return { return {
radius: 3, radius: 3,
@ -191,25 +199,48 @@ const loop = (now) => {
if (Object.keys(state.changes).length > 0) { if (Object.keys(state.changes).length > 0) {
if (state.changes.width || state.changes.height) { if (state.changes.width || state.changes.height) {
state.changes.rows = Math.min( state.changes.rows = Math.floor(
DEFAULTS.max_rows, Math.min(DEFAULTS.max_rows, state.changes.height / DEFAULTS.min_gap)
state.changes.height / DEFAULTS.min_gap
); );
state.changes.cols = Math.min( state.changes.cols = Math.floor(
DEFAULTS.max_cols, Math.min(DEFAULTS.max_cols, state.changes.width / DEFAULTS.min_gap)
state.changes.width / DEFAULTS.min_gap
); );
} }
state = { ...state, ...state.changes }; state = { ...state, ...state.changes };
state.changes = {}; state.changes = {};
} }
handle_input(state, dt); 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); render(state);
requestAnimationFrame(loop); 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 // DOM
const on_resize = () => { const on_resize = () => {
CANVAS.width = document.body.clientWidth; CANVAS.width = document.body.clientWidth;
@ -232,4 +263,14 @@ window.addEventListener("keyup", on_keyup);
// main // main
on_resize(); 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); requestAnimationFrame(loop);

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

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
const DEPTH = 22; const DEPTH = 15;
const DIRECTION = { const DIRECTION = {
0: new cx(0, 1), 0: new cx(0, 1),
@ -13,7 +13,7 @@ const backtrack = (local_index, depth) =>
.toString(2) .toString(2)
.padStart(depth, "0") .padStart(depth, "0")
.split("") .split("")
.map((direction) => (Number(direction) ? "-" : "+")); .map((direction) => (Number(direction) ? "+" : "-"));
const sol = (target, start_from = new cx(0, 0), start_to = new cx(1, 0)) => { 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 moves = [start_to, ...construct_moves(start_from, start_to)];

View File

@ -27,6 +27,20 @@
<a href="https://simponic.xyz">simponic.xyz</a>. <a href="https://simponic.xyz">simponic.xyz</a>.
</h3> </h3>
<div class="projects-grid"> <div class="projects-grid">
<div class="project" onclick="window.location='euler-golf/index.html'">
<div class="project-logo-container">
<i class="fa-solid fa-golf-ball-tee"></i>
</div>
<div class="project-body">
<h1>Euler Golf 2</h1>
<p>
A puzzle game (with solver) to explore rotations in the complex
plane.
</p>
</div>
</div>
<div class="project" onclick="window.location='julia/index.html'"> <div class="project" onclick="window.location='julia/index.html'">
<div class="project-logo-container"> <div class="project-logo-container">
<i class="fa-solid fa-square-root-variable"></i> <i class="fa-solid fa-square-root-variable"></i>