Finish euler golf
This commit is contained in:
parent
078a5c38b4
commit
8050a399df
@ -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;
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
</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/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
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 = {
|
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 = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.target) {
|
||||||
|
state.target = rand_target(state.rows, state.cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.solution) {
|
||||||
handle_input(state, dt);
|
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
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 = {
|
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)];
|
||||||
|
14
index.html
14
index.html
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user