Implement Sliding-Blocks and Restricted-Sliding-Blocks
This commit is contained in:
16
index.html
16
index.html
@ -1,9 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<!-- TODO: We want a toolbar at the top, the model visualization on the left -->
|
||||
<!-- and the graph on the right. -->
|
||||
<!-- The toolbar should have a model selector, an initial state editor, -->
|
||||
<!-- an initial state selector (from presets) and a generate button. -->
|
||||
<body style="margin: 5px">
|
||||
<div
|
||||
id="toolbar"
|
||||
@ -18,8 +14,14 @@
|
||||
gap: 5px;
|
||||
"
|
||||
>
|
||||
<button id="select_model_button">Select Model</button>
|
||||
<button id="select_model_button">Switch Model</button>
|
||||
<button disabled id="model_name"></button>
|
||||
<button id="select_state_button">Select State</button>
|
||||
<button disabled id="state_name"></button>
|
||||
<button id="up_button">UP</button>
|
||||
<button id="down_button">DOWN</button>
|
||||
<button id="left_button">LEFT</button>
|
||||
<button id="right_button">RIGHT</button>
|
||||
<button id="generate_graph_button">Generate Graph</button>
|
||||
<button id="reset_view_button">Reset View</button>
|
||||
<button id="clear_graph_button">Clear Graph</button>
|
||||
@ -39,7 +41,9 @@
|
||||
border-color: black;
|
||||
border-width: 1px;
|
||||
"
|
||||
></div>
|
||||
>
|
||||
<canvas id="model_canvas" style="width: 100%; height: 100%"></canvas>
|
||||
</div>
|
||||
<div
|
||||
id="graph"
|
||||
style="
|
||||
|
17
src/constants.js
Normal file
17
src/constants.js
Normal file
@ -0,0 +1,17 @@
|
||||
export const RIGHT = 0;
|
||||
export const DOWN = 1;
|
||||
export const LEFT = 2;
|
||||
export const UP = 3;
|
||||
export const DIRECTIONS = [
|
||||
[1, 0], // Right
|
||||
[0, 1], // Down
|
||||
[-1, 0], // Left
|
||||
[0, -1], // Up
|
||||
];
|
||||
|
||||
export const HORIZONTAL = 0;
|
||||
export const VERTICAL = 1;
|
||||
|
||||
export const invert_direction = (direction) => {
|
||||
return (DIRECTIONS.indexOf(direction) + 2) % DIRECTIONS.length;
|
||||
};
|
85
src/graph.js
85
src/graph.js
@ -1,44 +1,8 @@
|
||||
import ForceGraph3D from "3d-force-graph";
|
||||
import { get_viewport_dims } from "./viewport.js";
|
||||
import { states_are_equal } from "./simulator/sliding_blocks/state.js";
|
||||
|
||||
export const generate_sample_data = () => {
|
||||
// Example data:
|
||||
// {
|
||||
// "nodes": [
|
||||
// {
|
||||
// "id": "id1",
|
||||
// "name": "name1",
|
||||
// "val": 1
|
||||
// },
|
||||
// {
|
||||
// "id": "id2",
|
||||
// "name": "name2",
|
||||
// "val": 10
|
||||
// },
|
||||
// ...
|
||||
// ],
|
||||
// "links": [
|
||||
// {
|
||||
// "source": "id1",
|
||||
// "target": "id2"
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
let data = {
|
||||
nodes: [...Array(50).keys()].map((i) => ({ id: i })),
|
||||
links: [...Array(50).keys()]
|
||||
.filter((id) => id)
|
||||
.map((id) => ({
|
||||
source: id,
|
||||
target: Math.round(Math.random() * (id - 1)),
|
||||
})),
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const generate_graph = (data) => {
|
||||
// TODO: Highlight the current state by coloring the node
|
||||
export const generate_graph = (data, node_click_handler) => {
|
||||
let graph = ForceGraph3D()(document.getElementById("graph"))
|
||||
// Input the data into the graph
|
||||
.graphData(data)
|
||||
@ -47,38 +11,35 @@ export const generate_graph = (data) => {
|
||||
.backgroundColor("#FFFFFF")
|
||||
.nodeColor(["#555555"])
|
||||
.linkColor(["#000000"])
|
||||
.nodeRelSize([15])
|
||||
.nodeResolution([1])
|
||||
.linkResolution([1])
|
||||
|
||||
// Set up the interactions
|
||||
.onNodeHover(
|
||||
(node) => (document.body.style.cursor = node ? "pointer" : null),
|
||||
)
|
||||
.onNodeClick((node) => {
|
||||
// TODO: Visualize the clicked state in the GameView
|
||||
const distance = 40;
|
||||
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
|
||||
graph.cameraPosition(
|
||||
{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio },
|
||||
{ x: node.x, y: node.y, z: node.z },
|
||||
1000,
|
||||
);
|
||||
node_click_handler(node, graph);
|
||||
});
|
||||
|
||||
graph.d3Force("link").distance(35);
|
||||
// graph.warmupTicks([100]);
|
||||
// graph.cooldownTicks([0]);
|
||||
|
||||
reset_graph_view(graph);
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
const get_viewport_dims = () => {
|
||||
let vw = Math.max(
|
||||
document.documentElement.clientWidth || 0,
|
||||
window.innerWidth || 0,
|
||||
export const node_click_zoom = (node, graph) => {
|
||||
const distance = 40;
|
||||
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
|
||||
graph.cameraPosition(
|
||||
{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio },
|
||||
{ x: node.x, y: node.y, z: node.z },
|
||||
1000,
|
||||
);
|
||||
let vh = Math.max(
|
||||
document.documentElement.clientHeight || 0,
|
||||
window.innerHeight || 0,
|
||||
);
|
||||
|
||||
return [vw, vh];
|
||||
};
|
||||
|
||||
export const reset_graph_view = (graph) => {
|
||||
@ -92,3 +53,13 @@ export const reset_graph_view = (graph) => {
|
||||
.height(vh - 43)
|
||||
.zoomToFit();
|
||||
};
|
||||
|
||||
export const highlight_node = (states, graph, current_state) => {
|
||||
graph.nodeColor((node) => {
|
||||
if (states_are_equal(states[node.id], current_state)) {
|
||||
return "#FF0000";
|
||||
}
|
||||
|
||||
return "#555555";
|
||||
});
|
||||
};
|
||||
|
198
src/main.js
198
src/main.js
@ -1,28 +1,202 @@
|
||||
import "./graph.js";
|
||||
import {
|
||||
generate_graph,
|
||||
reset_graph_view,
|
||||
generate_sample_data,
|
||||
} from "./graph.js";
|
||||
import { generate_graph, reset_graph_view, highlight_node } from "./graph.js";
|
||||
import { DIRECTIONS, DOWN, LEFT, RIGHT, UP } from "./constants.js";
|
||||
import { restricted_sliding_blocks_model } from "./simulator/restricted_sliding_blocks/model.js";
|
||||
import { sliding_blocks_model } from "./simulator/sliding_blocks/model.js";
|
||||
import { get_viewport_dims } from "./viewport.js";
|
||||
|
||||
//
|
||||
// Application state
|
||||
//
|
||||
|
||||
let current_model = 0;
|
||||
let models = [restricted_sliding_blocks_model, sliding_blocks_model];
|
||||
let model = models[current_model];
|
||||
|
||||
let current_initial_state = 0;
|
||||
let initial_state = model.initial_states[current_initial_state];
|
||||
|
||||
let current_state = initial_state;
|
||||
let selected_element = null;
|
||||
|
||||
let states = null;
|
||||
let data = null;
|
||||
let graph = null;
|
||||
|
||||
const clear = () => {
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
const clear_graph = () => {
|
||||
document.getElementById("graph").innerHTML = "";
|
||||
clear_visualization();
|
||||
model.visualize(initial_state);
|
||||
states = [initial_state];
|
||||
current_state = initial_state;
|
||||
data = {
|
||||
nodes: [{ id: 0 }],
|
||||
links: [],
|
||||
};
|
||||
graph = null;
|
||||
data = null;
|
||||
graph = generate_graph(data, node_click_view_state);
|
||||
highlight_node(states, graph, current_state);
|
||||
};
|
||||
|
||||
const clear_visualization = () => {
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
const [vw, vh] = get_viewport_dims();
|
||||
canvas.width = vw / 2 - 9.5;
|
||||
canvas.height = vh - 43;
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
//
|
||||
// Set up the page when loaded
|
||||
//
|
||||
|
||||
window.onload = () => {
|
||||
document.getElementById("model_name").innerHTML = model.name;
|
||||
document.getElementById("state_name").innerHTML = initial_state.name;
|
||||
clear_visualization();
|
||||
model.visualize(initial_state);
|
||||
states = [initial_state];
|
||||
data = {
|
||||
nodes: [{ id: 0 }],
|
||||
links: [],
|
||||
};
|
||||
graph = generate_graph(data, node_click_view_state);
|
||||
highlight_node(states, graph, current_state);
|
||||
};
|
||||
|
||||
//
|
||||
// Node click handlers
|
||||
//
|
||||
|
||||
const node_click_view_state = (node, graph) => {
|
||||
clear_visualization();
|
||||
current_state = states[node.id];
|
||||
|
||||
model.visualize(current_state);
|
||||
highlight_node(states, graph, current_state);
|
||||
};
|
||||
|
||||
//
|
||||
// Set up model event-handlers
|
||||
//
|
||||
|
||||
document.getElementById("model_canvas").addEventListener("click", (event) => {
|
||||
const element = model.select(current_state, event.offsetX, event.offsetY);
|
||||
|
||||
if (element !== null) {
|
||||
clear_visualization();
|
||||
selected_element = element;
|
||||
|
||||
model.visualize(current_state);
|
||||
model.highlight(current_state, selected_element);
|
||||
}
|
||||
});
|
||||
|
||||
const move = (direction) => {
|
||||
if (selected_element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [state, element] = model.move(
|
||||
states,
|
||||
graph,
|
||||
current_state,
|
||||
selected_element,
|
||||
DIRECTIONS[direction],
|
||||
);
|
||||
|
||||
if (state !== null) {
|
||||
current_state = state;
|
||||
selected_element = element;
|
||||
clear_visualization();
|
||||
model.visualize(current_state);
|
||||
model.highlight(current_state, selected_element);
|
||||
highlight_node(states, graph, current_state);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById("up_button").addEventListener("click", () => {
|
||||
move(UP);
|
||||
});
|
||||
|
||||
document.getElementById("down_button").addEventListener("click", () => {
|
||||
move(DOWN);
|
||||
});
|
||||
|
||||
document.getElementById("left_button").addEventListener("click", () => {
|
||||
move(LEFT);
|
||||
});
|
||||
|
||||
document.getElementById("right_button").addEventListener("click", () => {
|
||||
move(RIGHT);
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (event) => {
|
||||
if (event.code === "ArrowUp" || event.code === "KeyW") {
|
||||
move(UP);
|
||||
} else if (event.code === "ArrowDown" || event.code === "KeyS") {
|
||||
move(DOWN);
|
||||
} else if (event.code === "ArrowLeft" || event.code === "KeyA") {
|
||||
move(LEFT);
|
||||
} else if (event.code === "ArrowRight" || event.code === "KeyD") {
|
||||
move(RIGHT);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Set up button event-handlers
|
||||
//
|
||||
|
||||
document.getElementById("select_model_button").addEventListener("click", () => {
|
||||
clear_graph();
|
||||
clear_visualization();
|
||||
current_model = (current_model + 1) % models.length;
|
||||
model = models[current_model];
|
||||
current_initial_state = 0;
|
||||
initial_state = model.initial_states[current_initial_state];
|
||||
selected_element = null;
|
||||
current_state = initial_state;
|
||||
states = [initial_state];
|
||||
graph = null;
|
||||
graph = generate_graph(data, node_click_view_state);
|
||||
model.visualize(initial_state);
|
||||
document.getElementById("model_name").innerHTML = model.name;
|
||||
document.getElementById("state_name").innerHTML = initial_state.name;
|
||||
});
|
||||
|
||||
document.getElementById("select_state_button").addEventListener("click", () => {
|
||||
clear_graph();
|
||||
clear_visualization();
|
||||
current_initial_state =
|
||||
(current_initial_state + 1) % model.initial_states.length;
|
||||
initial_state = model.initial_states[current_initial_state];
|
||||
current_state = initial_state;
|
||||
selected_element = null;
|
||||
states = [initial_state];
|
||||
graph = null;
|
||||
graph = generate_graph(data, node_click_view_state);
|
||||
model.visualize(initial_state);
|
||||
document.getElementById("state_name").innerHTML = initial_state.name;
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("generate_graph_button")
|
||||
.addEventListener("click", () => {
|
||||
clear();
|
||||
data = generate_sample_data();
|
||||
graph = generate_graph(data);
|
||||
clear_graph();
|
||||
model.generate(states, initial_state, graph, null);
|
||||
reset_graph_view(graph);
|
||||
});
|
||||
|
||||
document.getElementById("reset_view_button").addEventListener("click", () => {
|
||||
reset_graph_view(graph);
|
||||
});
|
||||
document.getElementById("clear_graph_button").addEventListener("click", clear);
|
||||
|
||||
document
|
||||
.getElementById("clear_graph_button")
|
||||
.addEventListener("click", clear_graph);
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { VERTICAL, HORIZONTAL } from "../../../constants.js";
|
||||
|
||||
export const cross_three = {
|
||||
name: "Cross Three",
|
||||
width: 7,
|
||||
height: 7,
|
||||
board: [
|
||||
[0, HORIZONTAL, 0, 2, 1, 2],
|
||||
[1, VERTICAL, 4, 0, 4, 1],
|
||||
[2, HORIZONTAL, 5, 4, 6, 4],
|
||||
],
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { VERTICAL, HORIZONTAL } from "../../../constants.js";
|
||||
|
||||
export const cross_two = {
|
||||
name: "Cross Two",
|
||||
width: 7,
|
||||
height: 7,
|
||||
board: [
|
||||
[0, HORIZONTAL, 0, 3, 1, 3],
|
||||
[1, VERTICAL, 3, 0, 3, 1],
|
||||
],
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import { VERTICAL } from "../../../constants.js";
|
||||
|
||||
export const five = {
|
||||
name: "Five",
|
||||
width: 5,
|
||||
height: 5,
|
||||
board: [
|
||||
[0, VERTICAL, 0, 0, 0, 1],
|
||||
[1, VERTICAL, 1, 0, 1, 1],
|
||||
[2, VERTICAL, 2, 0, 2, 1],
|
||||
[3, VERTICAL, 3, 0, 3, 1],
|
||||
[4, VERTICAL, 4, 0, 4, 1],
|
||||
],
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
import { VERTICAL } from "../../../constants.js";
|
||||
|
||||
export const four = {
|
||||
name: "Four",
|
||||
width: 5,
|
||||
height: 5,
|
||||
board: [
|
||||
[0, VERTICAL, 0, 0, 0, 1],
|
||||
[1, VERTICAL, 1, 0, 1, 1],
|
||||
[2, VERTICAL, 2, 0, 2, 1],
|
||||
[3, VERTICAL, 3, 0, 3, 1],
|
||||
],
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { VERTICAL, HORIZONTAL } from "../../../constants.js";
|
||||
|
||||
export const head_on = {
|
||||
name: "Head On",
|
||||
width: 7,
|
||||
height: 7,
|
||||
board: [
|
||||
[0, VERTICAL, 3, 0, 3, 1],
|
||||
[1, VERTICAL, 3, 5, 3, 6],
|
||||
],
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { VERTICAL } from "../../../constants.js";
|
||||
|
||||
export const single = {
|
||||
name: "Single",
|
||||
width: 5,
|
||||
height: 5,
|
||||
board: [[0, VERTICAL, 0, 0, 0, 1]],
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
import { VERTICAL } from "../../../constants.js";
|
||||
|
||||
export const three = {
|
||||
name: "Three",
|
||||
width: 5,
|
||||
height: 5,
|
||||
board: [
|
||||
[0, VERTICAL, 0, 0, 0, 1],
|
||||
[1, VERTICAL, 1, 0, 1, 1],
|
||||
[2, VERTICAL, 2, 0, 2, 1],
|
||||
],
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { VERTICAL } from "../../../constants.js";
|
||||
|
||||
export const two = {
|
||||
name: "Two",
|
||||
width: 5,
|
||||
height: 5,
|
||||
board: [
|
||||
[0, VERTICAL, 0, 0, 0, 1],
|
||||
[1, VERTICAL, 1, 0, 1, 1],
|
||||
],
|
||||
};
|
228
src/simulator/restricted_sliding_blocks/model.js
Normal file
228
src/simulator/restricted_sliding_blocks/model.js
Normal file
@ -0,0 +1,228 @@
|
||||
import { single } from "./initial_states/single.js";
|
||||
import { two } from "./initial_states/two.js";
|
||||
import { three } from "./initial_states/three.js";
|
||||
import { four } from "./initial_states/four.js";
|
||||
import { five } from "./initial_states/five.js";
|
||||
import { head_on } from "./initial_states/head_on.js";
|
||||
import { cross_two } from "./initial_states/cross_two.js";
|
||||
import { cross_three } from "./initial_states/cross_three.js";
|
||||
import {
|
||||
get_moves,
|
||||
block_is_movable,
|
||||
index_of_state,
|
||||
remove_block,
|
||||
move_block,
|
||||
insert_block,
|
||||
move_state_block,
|
||||
arrays_are_equal,
|
||||
} from "./state.js";
|
||||
import { DIRECTIONS, LEFT, RIGHT, UP, DOWN } from "../../constants.js";
|
||||
import { HORIZONTAL, VERTICAL } from "../../constants.js";
|
||||
|
||||
const initial_states = [
|
||||
single,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
// five,
|
||||
head_on,
|
||||
cross_two,
|
||||
cross_three,
|
||||
];
|
||||
|
||||
const generate = (states, initial_state, graph, previous_move) => {
|
||||
const stack = [[initial_state, previous_move]];
|
||||
|
||||
let last_print = 0;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [current_state, prev_move] = stack.pop();
|
||||
|
||||
const moves = get_moves(prev_move, current_state);
|
||||
for (const m of moves) {
|
||||
const [block, direction] = m;
|
||||
|
||||
const states_before = states.length;
|
||||
const [new_state, new_block] = move(
|
||||
states,
|
||||
graph,
|
||||
current_state,
|
||||
block,
|
||||
direction,
|
||||
);
|
||||
const states_after = states.length;
|
||||
|
||||
if (states_after - last_print > 250) {
|
||||
console.log(`Generating: Found ${states_after} states...`);
|
||||
last_print = states_after;
|
||||
}
|
||||
|
||||
if (states_after > states_before) {
|
||||
stack.push([new_state, m]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const select = (state, offsetX, offsetY) => {
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
const x = Math.floor(offsetX / square_width);
|
||||
const y = Math.floor(offsetY / square_height);
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const [id, dir, x0, y0, x1, y1] = state.board[i];
|
||||
|
||||
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
|
||||
return [id, dir, x0, y0, x1, y1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const move = (states, graph, state, block, direction) => {
|
||||
if (
|
||||
block[1] === HORIZONTAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[UP]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[DOWN]))
|
||||
) {
|
||||
console.log("Can't move block horizontally");
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (
|
||||
block[1] === VERTICAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[LEFT]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[RIGHT]))
|
||||
) {
|
||||
console.log("Can't move block vertically");
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (!block_is_movable(state, block, direction)) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// let new_state = remove_block(state, block);
|
||||
// let new_block = move_block(block, direction);
|
||||
// insert_block(new_state, block);
|
||||
|
||||
let new_state = structuredClone(state);
|
||||
let new_block = move_block(block, direction);
|
||||
move_state_block(new_state, block, direction);
|
||||
|
||||
// TODO: Make states into a hashmap?
|
||||
let index = index_of_state(states, new_state);
|
||||
|
||||
let new_link = null;
|
||||
let new_node = null;
|
||||
if (index !== null) {
|
||||
// We already had this state, just generate a link
|
||||
new_link = {
|
||||
source: index_of_state(states, state), // We're coming from this state...
|
||||
target: index, // ...and ended up here, at a previous state.
|
||||
};
|
||||
} else {
|
||||
states.push(new_state);
|
||||
new_node = {
|
||||
id: states.length - 1,
|
||||
};
|
||||
new_link = {
|
||||
source: index_of_state(states, state), // We're coming from this state...
|
||||
target: states.length - 1, // ...and ended up here, at a new state.
|
||||
};
|
||||
}
|
||||
|
||||
const data = graph.graphData();
|
||||
|
||||
// TODO: Faster without this?
|
||||
const has_link = (data, link) => {
|
||||
// for (let l of data.links) {
|
||||
// if (l.source.id === link.source && l.target.id === link.target) {
|
||||
// return true;
|
||||
// }
|
||||
// if (l.source.id === link.target && l.target.id === link.source) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (new_node !== null) {
|
||||
graph.graphData({
|
||||
nodes: [...data.nodes, new_node],
|
||||
links: [...data.links, new_link],
|
||||
});
|
||||
} else if (!has_link(data, new_link)) {
|
||||
graph.graphData({
|
||||
nodes: data.nodes,
|
||||
links: [...data.links, new_link],
|
||||
});
|
||||
}
|
||||
|
||||
return [new_state, new_block];
|
||||
};
|
||||
|
||||
const rect = (context, x0, y0, x1, y1, square_width, square_height, color) => {
|
||||
const x = x0 * square_width;
|
||||
const y = y0 * square_height;
|
||||
const width = (x1 - x0 + 1) * square_width;
|
||||
const height = (y1 - y0 + 1) * square_height;
|
||||
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
context.strokeStyle = "#000000";
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(x, y, width, height);
|
||||
};
|
||||
|
||||
const visualize = (state) => {
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
// console.log(`Canvas: (${canvas.width}x${canvas.height})`);
|
||||
// console.log(`Klotski 1x1 Size: (${square_width}x${square_height})`);
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const [id, dir, x0, y0, x1, y1] = state.board[i];
|
||||
|
||||
rect(context, x0, y0, x1, y1, square_width, square_height, "#555555");
|
||||
}
|
||||
};
|
||||
|
||||
const highlight = (state, block) => {
|
||||
const [id, dir, x0, y0, x1, y1] = block;
|
||||
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
rect(context, x0, y0, x1, y1, square_width, square_height, "#AAAAAA");
|
||||
};
|
||||
|
||||
const visualize_path = (from_state, to_state) => {
|
||||
// Find path (general graph helper function)
|
||||
// For each state in path: visualize(state)
|
||||
};
|
||||
|
||||
export const restricted_sliding_blocks_model = {
|
||||
name: "Restricted Sliding Blocks",
|
||||
generate,
|
||||
visualize,
|
||||
highlight,
|
||||
visualize_path,
|
||||
initial_states,
|
||||
select,
|
||||
move,
|
||||
};
|
251
src/simulator/restricted_sliding_blocks/state.js
Normal file
251
src/simulator/restricted_sliding_blocks/state.js
Normal file
@ -0,0 +1,251 @@
|
||||
import {
|
||||
RIGHT,
|
||||
DOWN,
|
||||
LEFT,
|
||||
UP,
|
||||
DIRECTIONS,
|
||||
invert_direction,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
} from "../../constants.js";
|
||||
|
||||
export const arrays_are_equal = (array, other_array) => {
|
||||
if (array.length != other_array.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] !== other_array[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const state_contains_block = (state, block) => {
|
||||
return (
|
||||
state.board.length >= block[0] &&
|
||||
arrays_are_equal(state.board[block[0]], block)
|
||||
);
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const other_block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (arrays_are_equal(block, other_block)) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
};
|
||||
|
||||
export const states_are_equal = (state, other_state) => {
|
||||
if (state.board.length != other_state.board.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
if (!arrays_are_equal(state.board[i], other_state.board[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (!state_contains_block(other_state, block)) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
};
|
||||
|
||||
export const index_of_state = (states, state) => {
|
||||
for (let i = 0; i < states.length; i++) {
|
||||
if (states_are_equal(states[i], state)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const remove_block = (state, block) => {
|
||||
let new_state = structuredClone(state);
|
||||
|
||||
new_state.board.splice(block[0], 1);
|
||||
|
||||
return new_state;
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const other_block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (arrays_are_equal(block, other_block)) {
|
||||
// new_state.board.splice(i * 4, 4);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new_state;
|
||||
};
|
||||
|
||||
export const insert_block = (state, block) => {
|
||||
state.board.splice(block[0], 0, block);
|
||||
};
|
||||
|
||||
export const move_block = (block, direction) => {
|
||||
const [id, dir, x0, y0, x1, y1] = block;
|
||||
const [dx, dy] = direction;
|
||||
|
||||
if (
|
||||
dir === HORIZONTAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[UP]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[DOWN]))
|
||||
) {
|
||||
console.log("Can't move block horizontally");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
dir === VERTICAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[LEFT]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[RIGHT]))
|
||||
) {
|
||||
console.log("Can't move block vertically");
|
||||
return null;
|
||||
}
|
||||
|
||||
return [id, dir, x0 + dx, y0 + dy, x1 + dx, y1 + dy];
|
||||
};
|
||||
|
||||
export const move_state_block = (state, block, direction) => {
|
||||
const [id, dir, x0, y0, x1, y1] = state.board[block[0]];
|
||||
const [dx, dy] = direction;
|
||||
|
||||
if (
|
||||
dir === HORIZONTAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[UP]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[DOWN]))
|
||||
) {
|
||||
console.log("Can't move block horizontally");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
dir === VERTICAL &&
|
||||
(arrays_are_equal(direction, DIRECTIONS[LEFT]) ||
|
||||
arrays_are_equal(direction, DIRECTIONS[RIGHT]))
|
||||
) {
|
||||
console.log("Can't move block vertically");
|
||||
return;
|
||||
}
|
||||
|
||||
state.board[block[0]] = [id, dir, x0 + dx, y0 + dy, x1 + dx, y1 + dy];
|
||||
};
|
||||
|
||||
export const block_collides_with_other_block = (block, other_block) => {
|
||||
const [id0, dir0, x0, y0, x1, y1] = block;
|
||||
const [id1, dir1, x2, y2, x3, y3] = other_block;
|
||||
|
||||
// Don't check for self-collisions
|
||||
if (id0 === id1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Creates a set containing all x or y coordinates occupied by the block
|
||||
const span = (a0, a1) => {
|
||||
const start = Math.min(a0, a1);
|
||||
const end = Math.max(a0, a1);
|
||||
|
||||
let set = new Set();
|
||||
for (let i = start; i <= end; i++) {
|
||||
set.add(i);
|
||||
}
|
||||
return set;
|
||||
};
|
||||
|
||||
// Checks if two sets intersect
|
||||
const intersects = (set0, set1) => {
|
||||
for (const e of set0)
|
||||
if (set1.has(e)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const xs0 = span(x0, x1);
|
||||
const ys0 = span(y0, y1);
|
||||
const xs1 = span(x2, x3);
|
||||
const ys1 = span(y2, y3);
|
||||
|
||||
// If block and other_block have a shared x and y coordinate, they intersect
|
||||
return intersects(xs0, xs1) && intersects(ys0, ys1);
|
||||
};
|
||||
|
||||
export const block_collides = (state, block) => {
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const other_block = state.board[i];
|
||||
|
||||
if (block_collides_with_other_block(block, other_block)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const block_is_movable = (state, block, direction) => {
|
||||
// Move the block, then check if the block would intersect or collide
|
||||
const [id, dir, x0, y0, x1, y1] = move_block(block, direction);
|
||||
|
||||
// Check collisions with board borders
|
||||
if (x0 < 0 || x1 >= state.width) {
|
||||
return false;
|
||||
}
|
||||
if (y0 < 0 || y1 >= state.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check collisions with other blocks.
|
||||
if (block_collides(state, [id, dir, x0, y0, x1, y1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const get_moves = (last_move, state) => {
|
||||
// Example move: [block, direction]
|
||||
let moves = [];
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const block = state.board[i];
|
||||
|
||||
let dirs;
|
||||
if (block[1] === VERTICAL) {
|
||||
dirs = [DIRECTIONS[UP], DIRECTIONS[DOWN]];
|
||||
} else if (block[1] === HORIZONTAL) {
|
||||
dirs = [DIRECTIONS[LEFT], DIRECTIONS[RIGHT]];
|
||||
}
|
||||
|
||||
for (let direction of dirs) {
|
||||
if (
|
||||
last_move !== null &&
|
||||
arrays_are_equal(last_move[0], block) &&
|
||||
arrays_are_equal(last_move[1], invert_direction(direction))
|
||||
) {
|
||||
// We don't want to move the block back to where we came from...
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block_is_movable(state, block, direction)) {
|
||||
moves.push([block, direction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
};
|
11
src/simulator/sliding_blocks/initial_states/four.js
Normal file
11
src/simulator/sliding_blocks/initial_states/four.js
Normal file
@ -0,0 +1,11 @@
|
||||
export const four = {
|
||||
name: "Four",
|
||||
width: 3,
|
||||
height: 3,
|
||||
board: [
|
||||
[0, 0, 0, 0, 0],
|
||||
[1, 2, 0, 2, 0],
|
||||
[2, 0, 2, 0, 2],
|
||||
[3, 2, 2, 2, 2],
|
||||
],
|
||||
};
|
20
src/simulator/sliding_blocks/initial_states/klotski.js
Normal file
20
src/simulator/sliding_blocks/initial_states/klotski.js
Normal file
@ -0,0 +1,20 @@
|
||||
export const klotski = {
|
||||
name: "Klotski",
|
||||
width: 4,
|
||||
height: 5,
|
||||
board: [
|
||||
// Center column
|
||||
[0, 1, 0, 2, 1],
|
||||
[1, 1, 2, 2, 2],
|
||||
[2, 1, 3, 1, 3],
|
||||
[3, 2, 3, 2, 3],
|
||||
// Left column
|
||||
[4, 0, 0, 0, 1],
|
||||
[5, 0, 2, 0, 3],
|
||||
[6, 0, 4, 0, 4],
|
||||
// Right column
|
||||
[7, 3, 0, 3, 1],
|
||||
[8, 3, 2, 3, 3],
|
||||
[9, 3, 4, 3, 4],
|
||||
],
|
||||
};
|
6
src/simulator/sliding_blocks/initial_states/single.js
Normal file
6
src/simulator/sliding_blocks/initial_states/single.js
Normal file
@ -0,0 +1,6 @@
|
||||
export const single = {
|
||||
name: "Single",
|
||||
width: 4,
|
||||
height: 4,
|
||||
board: [[0, 0, 0, 0, 0]],
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
export const small_klotski_like = {
|
||||
name: "Small Klotski Like",
|
||||
width: 4,
|
||||
height: 4,
|
||||
board: [
|
||||
[0, 0, 0, 1, 0],
|
||||
[1, 0, 1, 0, 2],
|
||||
[2, 0, 3, 0, 3],
|
||||
[3, 1, 1, 2, 2],
|
||||
[4, 1, 3, 2, 3],
|
||||
[5, 3, 0, 3, 0],
|
||||
[6, 3, 1, 3, 1],
|
||||
[7, 3, 2, 3, 2],
|
||||
],
|
||||
};
|
10
src/simulator/sliding_blocks/initial_states/three.js
Normal file
10
src/simulator/sliding_blocks/initial_states/three.js
Normal file
@ -0,0 +1,10 @@
|
||||
export const three = {
|
||||
name: "Three",
|
||||
width: 3,
|
||||
height: 3,
|
||||
board: [
|
||||
[0, 0, 0, 0, 0],
|
||||
[1, 2, 0, 2, 0],
|
||||
[2, 0, 2, 0, 2],
|
||||
],
|
||||
};
|
9
src/simulator/sliding_blocks/initial_states/two.js
Normal file
9
src/simulator/sliding_blocks/initial_states/two.js
Normal file
@ -0,0 +1,9 @@
|
||||
export const two = {
|
||||
name: "Two",
|
||||
width: 3,
|
||||
height: 3,
|
||||
board: [
|
||||
[0, 0, 0, 0, 0],
|
||||
[1, 2, 0, 2, 0],
|
||||
],
|
||||
};
|
202
src/simulator/sliding_blocks/model.js
Normal file
202
src/simulator/sliding_blocks/model.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { single } from "./initial_states/single.js";
|
||||
import { two } from "./initial_states/two.js";
|
||||
import { three } from "./initial_states/three.js";
|
||||
import { four } from "./initial_states/four.js";
|
||||
import { small_klotski_like } from "./initial_states/small_klotski_like.js";
|
||||
import { klotski } from "./initial_states/klotski.js";
|
||||
import {
|
||||
get_moves,
|
||||
block_is_movable,
|
||||
index_of_state,
|
||||
remove_block,
|
||||
move_block,
|
||||
insert_block,
|
||||
move_state_block,
|
||||
} from "./state.js";
|
||||
|
||||
const initial_states = [single, two, three, four, small_klotski_like, klotski];
|
||||
|
||||
const generate = (states, initial_state, graph, previous_move) => {
|
||||
const stack = [[initial_state, previous_move]];
|
||||
|
||||
let last_print = 0;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [current_state, prev_move] = stack.pop();
|
||||
|
||||
const moves = get_moves(prev_move, current_state);
|
||||
for (const m of moves) {
|
||||
const [block, direction] = m;
|
||||
|
||||
const states_before = states.length;
|
||||
const [new_state, new_block] = move(
|
||||
states,
|
||||
graph,
|
||||
current_state,
|
||||
block,
|
||||
direction,
|
||||
);
|
||||
const states_after = states.length;
|
||||
|
||||
if (states_after - last_print > 250) {
|
||||
console.log(`Generating: Found ${states_after} states...`);
|
||||
last_print = states_after;
|
||||
}
|
||||
|
||||
if (states_after > states_before) {
|
||||
stack.push([new_state, m]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const select = (state, offsetX, offsetY) => {
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
const x = Math.floor(offsetX / square_width);
|
||||
const y = Math.floor(offsetY / square_height);
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const [id, x0, y0, x1, y1] = state.board[i];
|
||||
|
||||
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
|
||||
return [id, x0, y0, x1, y1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const move = (states, graph, state, block, direction) => {
|
||||
if (!block_is_movable(state, block, direction)) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// let new_state = remove_block(state, block);
|
||||
// let new_block = move_block(block, direction);
|
||||
// insert_block(new_state, block);
|
||||
|
||||
let new_state;
|
||||
try {
|
||||
new_state = structuredClone(state);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return [null, null];
|
||||
}
|
||||
let new_block = move_block(block, direction);
|
||||
move_state_block(new_state, block, direction);
|
||||
|
||||
// TODO: Make states into a hashmap?
|
||||
let index = index_of_state(states, new_state);
|
||||
|
||||
let new_link = null;
|
||||
let new_node = null;
|
||||
if (index !== null) {
|
||||
// We already had this state, just generate a link
|
||||
new_link = {
|
||||
source: index_of_state(states, state), // We're coming from this state...
|
||||
target: index, // ...and ended up here, at a previous state.
|
||||
};
|
||||
} else {
|
||||
states.push(new_state);
|
||||
new_node = {
|
||||
id: states.length - 1,
|
||||
};
|
||||
new_link = {
|
||||
source: index_of_state(states, state), // We're coming from this state...
|
||||
target: states.length - 1, // ...and ended up here, at a new state.
|
||||
};
|
||||
}
|
||||
|
||||
const data = graph.graphData();
|
||||
|
||||
// TODO: Faster without this?
|
||||
const has_link = (data, link) => {
|
||||
// for (let l of data.links) {
|
||||
// if (l.source.id === link.source && l.target.id === link.target) {
|
||||
// return true;
|
||||
// }
|
||||
// if (l.source.id === link.target && l.target.id === link.source) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (new_node !== null) {
|
||||
graph.graphData({
|
||||
nodes: [...data.nodes, new_node],
|
||||
links: [...data.links, new_link],
|
||||
});
|
||||
} else if (!has_link(data, new_link)) {
|
||||
graph.graphData({
|
||||
nodes: data.nodes,
|
||||
links: [...data.links, new_link],
|
||||
});
|
||||
}
|
||||
|
||||
return [new_state, new_block];
|
||||
};
|
||||
|
||||
const rect = (context, x0, y0, x1, y1, square_width, square_height, color) => {
|
||||
const x = x0 * square_width;
|
||||
const y = y0 * square_height;
|
||||
const width = (x1 - x0 + 1) * square_width;
|
||||
const height = (y1 - y0 + 1) * square_height;
|
||||
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, y, width, height);
|
||||
|
||||
context.strokeStyle = "#000000";
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(x, y, width, height);
|
||||
};
|
||||
|
||||
const visualize = (state) => {
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
// console.log(`Canvas: (${canvas.width}x${canvas.height})`);
|
||||
// console.log(`Klotski 1x1 Size: (${square_width}x${square_height})`);
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const [id, x0, y0, x1, y1] = state.board[i];
|
||||
|
||||
rect(context, x0, y0, x1, y1, square_width, square_height, "#555555");
|
||||
}
|
||||
};
|
||||
|
||||
const highlight = (state, block) => {
|
||||
const [id, x0, y0, x1, y1] = block;
|
||||
|
||||
const canvas = document.getElementById("model_canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
const square_width = canvas.width / state.width;
|
||||
const square_height = canvas.height / state.height;
|
||||
|
||||
rect(context, x0, y0, x1, y1, square_width, square_height, "#AAAAAA");
|
||||
};
|
||||
|
||||
const visualize_path = (from_state, to_state) => {
|
||||
// Find path (general graph helper function)
|
||||
// For each state in path: visualize(state)
|
||||
};
|
||||
|
||||
export const sliding_blocks_model = {
|
||||
name: "Sliding Blocks",
|
||||
generate,
|
||||
visualize,
|
||||
highlight,
|
||||
visualize_path,
|
||||
initial_states,
|
||||
select,
|
||||
move,
|
||||
};
|
207
src/simulator/sliding_blocks/state.js
Normal file
207
src/simulator/sliding_blocks/state.js
Normal file
@ -0,0 +1,207 @@
|
||||
import {
|
||||
RIGHT,
|
||||
DOWN,
|
||||
LEFT,
|
||||
UP,
|
||||
DIRECTIONS,
|
||||
invert_direction,
|
||||
} from "../../constants.js";
|
||||
|
||||
export const arrays_are_equal = (array, other_array) => {
|
||||
if (array.length != other_array.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] !== other_array[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const state_contains_block = (state, block) => {
|
||||
return (
|
||||
state.board.length >= block[0] &&
|
||||
arrays_are_equal(state.board[block[0]], block)
|
||||
);
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const other_block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (arrays_are_equal(block, other_block)) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
};
|
||||
|
||||
export const states_are_equal = (state, other_state) => {
|
||||
if (state.board.length != other_state.board.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
if (!arrays_are_equal(state.board[i], other_state.board[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (!state_contains_block(other_state, block)) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
};
|
||||
|
||||
export const index_of_state = (states, state) => {
|
||||
for (let i = 0; i < states.length; i++) {
|
||||
if (states_are_equal(states[i], state)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const remove_block = (state, block) => {
|
||||
let new_state = structuredClone(state);
|
||||
|
||||
new_state.board.splice(block[0], 1);
|
||||
|
||||
return new_state;
|
||||
|
||||
// for (let i = 0; i < state.board.length / 4; i++) {
|
||||
// const other_block = state.board.slice(i * 4, (i + 1) * 4);
|
||||
//
|
||||
// if (arrays_are_equal(block, other_block)) {
|
||||
// new_state.board.splice(i * 4, 4);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return new_state;
|
||||
};
|
||||
|
||||
export const insert_block = (state, block) => {
|
||||
state.board.splice(block[0], 0, block);
|
||||
};
|
||||
|
||||
export const move_block = (block, direction) => {
|
||||
const [id, x0, y0, x1, y1] = block;
|
||||
const [dx, dy] = direction;
|
||||
|
||||
return [id, x0 + dx, y0 + dy, x1 + dx, y1 + dy];
|
||||
};
|
||||
|
||||
export const move_state_block = (state, block, direction) => {
|
||||
const [id, x0, y0, x1, y1] = state.board[block[0]];
|
||||
const [dx, dy] = direction;
|
||||
|
||||
state.board[block[0]] = [id, x0 + dx, y0 + dy, x1 + dx, y1 + dy];
|
||||
};
|
||||
|
||||
export const block_collides_with_other_block = (block, other_block) => {
|
||||
const [id0, x0, y0, x1, y1] = block;
|
||||
const [id1, x2, y2, x3, y3] = other_block;
|
||||
|
||||
// Don't check for self-collisions
|
||||
if (id0 === id1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Creates a set containing all x or y coordinates occupied by the block
|
||||
const span = (a0, a1) => {
|
||||
const start = Math.min(a0, a1);
|
||||
const end = Math.max(a0, a1);
|
||||
|
||||
let set = new Set();
|
||||
for (let i = start; i <= end; i++) {
|
||||
set.add(i);
|
||||
}
|
||||
return set;
|
||||
};
|
||||
|
||||
// Checks if two sets intersect
|
||||
const intersects = (set0, set1) => {
|
||||
for (const e of set0)
|
||||
if (set1.has(e)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const xs0 = span(x0, x1);
|
||||
const ys0 = span(y0, y1);
|
||||
const xs1 = span(x2, x3);
|
||||
const ys1 = span(y2, y3);
|
||||
|
||||
// If block and other_block have a shared x and y coordinate, they intersect
|
||||
return intersects(xs0, xs1) && intersects(ys0, ys1);
|
||||
};
|
||||
|
||||
export const block_collides = (state, block) => {
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const other_block = state.board[i];
|
||||
|
||||
if (block_collides_with_other_block(block, other_block)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const block_is_movable = (state, block, direction) => {
|
||||
// Move the block, then check if the block would intersect or collide
|
||||
const [id, x0, y0, x1, y1] = move_block(block, direction);
|
||||
|
||||
// Check collisions with board borders
|
||||
if (x0 < 0 || x1 >= state.width) {
|
||||
return false;
|
||||
}
|
||||
if (y0 < 0 || y1 >= state.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check collisions with other blocks.
|
||||
// We remove the block being checked from the board so it doesn't self-collide,
|
||||
if (block_collides(state, [id, x0, y0, x1, y1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const get_moves = (last_move, state) => {
|
||||
// Example move: [block, direction]
|
||||
let moves = [];
|
||||
|
||||
for (let i = 0; i < state.board.length; i++) {
|
||||
const block = state.board[i];
|
||||
|
||||
for (let direction of DIRECTIONS) {
|
||||
if (
|
||||
last_move !== null &&
|
||||
arrays_are_equal(last_move[0], block) &&
|
||||
arrays_are_equal(last_move[1], invert_direction(direction))
|
||||
) {
|
||||
// We don't want to move the block back to where we came from...
|
||||
continue;
|
||||
}
|
||||
|
||||
if (block_is_movable(state, block, direction)) {
|
||||
moves.push([block, direction]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
};
|
12
src/viewport.js
Normal file
12
src/viewport.js
Normal file
@ -0,0 +1,12 @@
|
||||
export const get_viewport_dims = () => {
|
||||
let vw = Math.max(
|
||||
document.documentElement.clientWidth || 0,
|
||||
window.innerWidth || 0,
|
||||
);
|
||||
let vh = Math.max(
|
||||
document.documentElement.clientHeight || 0,
|
||||
window.innerHeight || 0,
|
||||
);
|
||||
|
||||
return [vw, vh];
|
||||
};
|
Reference in New Issue
Block a user