Euler golf #1
@ -24,21 +24,51 @@ body {
|
||||
height: 100vw;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid black;
|
||||
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.5);
|
||||
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;
|
||||
}
|
||||
|
@ -7,13 +7,68 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="controls">
|
||||
<span id="iteration-count"></span>
|
||||
<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
32
euler-golf/js/controls.js
vendored
Normal 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());
|
@ -1,18 +1,16 @@
|
||||
const DEFAULTS = {
|
||||
max_rows: 80,
|
||||
max_cols: 80,
|
||||
min_gap: 40,
|
||||
min_gap: 30,
|
||||
angle_multiplier: 10e-4,
|
||||
};
|
||||
const CANVAS = document.getElementById("canvas");
|
||||
|
||||
let state = {
|
||||
grid_padding: 10,
|
||||
grid_padding: 30,
|
||||
canvas: CANVAS,
|
||||
ctx: CANVAS.getContext("2d"),
|
||||
last_render: 0,
|
||||
path: [new cx(0, 0), new cx(1, 0)],
|
||||
angle: new cx(0, 0),
|
||||
keys: {},
|
||||
changes: {},
|
||||
};
|
||||
@ -59,7 +57,7 @@ CanvasRenderingContext2D.prototype.draw_cartesian_path = function (
|
||||
CanvasRenderingContext2D.prototype.do_grid = function (
|
||||
rows,
|
||||
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 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) =>
|
||||
Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
const rand_target = (cols, rows) => {
|
||||
const res = new cx(rand_between(0, cols), rand_between(0, rows));
|
||||
if (res.re % 2 || res.im % 2) return rand_target(cols, rows);
|
||||
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;
|
||||
};
|
||||
@ -121,21 +121,26 @@ const complex_to_grid = (c, rows, cols) => {
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
state = maybe_add_state_angle_move(state);
|
||||
};
|
||||
|
||||
const render = ({ width, height, ctx, rows, cols } = 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);
|
||||
@ -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),
|
||||
]);
|
||||
|
||||
ctx.line(
|
||||
grid_to_canvas(complex_to_grid(curr, rows, cols), grid_spec),
|
||||
grid_to_canvas(
|
||||
complex_to_grid(
|
||||
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)),
|
||||
rows,
|
||||
cols
|
||||
),
|
||||
grid_spec
|
||||
),
|
||||
6,
|
||||
"#aaa"
|
||||
);
|
||||
].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)) {
|
||||
@ -176,6 +179,11 @@ const render = ({ width, height, ctx, rows, cols } = state) => {
|
||||
radius: 7,
|
||||
color: "#2f9c94",
|
||||
};
|
||||
} else if (x == grid_target.x && y == grid_target.y) {
|
||||
return {
|
||||
radius: 8,
|
||||
color: "#00ff00",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
radius: 3,
|
||||
@ -191,25 +199,48 @@ const loop = (now) => {
|
||||
|
||||
if (Object.keys(state.changes).length > 0) {
|
||||
if (state.changes.width || state.changes.height) {
|
||||
state.changes.rows = Math.min(
|
||||
DEFAULTS.max_rows,
|
||||
state.changes.height / DEFAULTS.min_gap
|
||||
state.changes.rows = Math.floor(
|
||||
Math.min(DEFAULTS.max_rows, state.changes.height / DEFAULTS.min_gap)
|
||||
);
|
||||
state.changes.cols = Math.min(
|
||||
DEFAULTS.max_cols,
|
||||
state.changes.width / 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;
|
||||
@ -232,4 +263,14 @@ 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);
|
||||
|
1
euler-golf/js/modal-vanilla.min.js
vendored
Normal file
1
euler-golf/js/modal-vanilla.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
const DEPTH = 22;
|
||||
const DEPTH = 15;
|
||||
|
||||
const DIRECTION = {
|
||||
0: new cx(0, 1),
|
||||
@ -13,7 +13,7 @@ const backtrack = (local_index, depth) =>
|
||||
.toString(2)
|
||||
.padStart(depth, "0")
|
||||
.split("")
|
||||
.map((direction) => (Number(direction) ? "-" : "+"));
|
||||
.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)];
|
||||
|
14
index.html
14
index.html
@ -27,6 +27,20 @@
|
||||
<a href="https://simponic.xyz">simponic.xyz</a>.
|
||||
</h3>
|
||||
<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-logo-container">
|
||||
<i class="fa-solid fa-square-root-variable"></i>
|
||||
|
Loading…
Reference in New Issue
Block a user