Implement Sliding-Blocks and Restricted-Sliding-Blocks
This commit is contained in:
16
index.html
16
index.html
@ -1,9 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<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">
|
<body style="margin: 5px">
|
||||||
<div
|
<div
|
||||||
id="toolbar"
|
id="toolbar"
|
||||||
@ -18,8 +14,14 @@
|
|||||||
gap: 5px;
|
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 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="generate_graph_button">Generate Graph</button>
|
||||||
<button id="reset_view_button">Reset View</button>
|
<button id="reset_view_button">Reset View</button>
|
||||||
<button id="clear_graph_button">Clear Graph</button>
|
<button id="clear_graph_button">Clear Graph</button>
|
||||||
@ -39,7 +41,9 @@
|
|||||||
border-color: black;
|
border-color: black;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
"
|
"
|
||||||
></div>
|
>
|
||||||
|
<canvas id="model_canvas" style="width: 100%; height: 100%"></canvas>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
id="graph"
|
id="graph"
|
||||||
style="
|
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 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 = () => {
|
export const generate_graph = (data, node_click_handler) => {
|
||||||
// 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
|
|
||||||
let graph = ForceGraph3D()(document.getElementById("graph"))
|
let graph = ForceGraph3D()(document.getElementById("graph"))
|
||||||
// Input the data into the graph
|
// Input the data into the graph
|
||||||
.graphData(data)
|
.graphData(data)
|
||||||
@ -47,38 +11,35 @@ export const generate_graph = (data) => {
|
|||||||
.backgroundColor("#FFFFFF")
|
.backgroundColor("#FFFFFF")
|
||||||
.nodeColor(["#555555"])
|
.nodeColor(["#555555"])
|
||||||
.linkColor(["#000000"])
|
.linkColor(["#000000"])
|
||||||
|
.nodeRelSize([15])
|
||||||
|
.nodeResolution([1])
|
||||||
|
.linkResolution([1])
|
||||||
|
|
||||||
// Set up the interactions
|
// Set up the interactions
|
||||||
.onNodeHover(
|
.onNodeHover(
|
||||||
(node) => (document.body.style.cursor = node ? "pointer" : null),
|
(node) => (document.body.style.cursor = node ? "pointer" : null),
|
||||||
)
|
)
|
||||||
.onNodeClick((node) => {
|
.onNodeClick((node) => {
|
||||||
// TODO: Visualize the clicked state in the GameView
|
node_click_handler(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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
graph.d3Force("link").distance(35);
|
||||||
|
// graph.warmupTicks([100]);
|
||||||
|
// graph.cooldownTicks([0]);
|
||||||
|
|
||||||
reset_graph_view(graph);
|
reset_graph_view(graph);
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
};
|
};
|
||||||
|
|
||||||
const get_viewport_dims = () => {
|
export const node_click_zoom = (node, graph) => {
|
||||||
let vw = Math.max(
|
const distance = 40;
|
||||||
document.documentElement.clientWidth || 0,
|
const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
|
||||||
window.innerWidth || 0,
|
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) => {
|
export const reset_graph_view = (graph) => {
|
||||||
@ -92,3 +53,13 @@ export const reset_graph_view = (graph) => {
|
|||||||
.height(vh - 43)
|
.height(vh - 43)
|
||||||
.zoomToFit();
|
.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, highlight_node } from "./graph.js";
|
||||||
import {
|
import { DIRECTIONS, DOWN, LEFT, RIGHT, UP } from "./constants.js";
|
||||||
generate_graph,
|
import { restricted_sliding_blocks_model } from "./simulator/restricted_sliding_blocks/model.js";
|
||||||
reset_graph_view,
|
import { sliding_blocks_model } from "./simulator/sliding_blocks/model.js";
|
||||||
generate_sample_data,
|
import { get_viewport_dims } from "./viewport.js";
|
||||||
} from "./graph.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 data = null;
|
||||||
let graph = null;
|
let graph = null;
|
||||||
|
|
||||||
const clear = () => {
|
//
|
||||||
|
// Helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
const clear_graph = () => {
|
||||||
document.getElementById("graph").innerHTML = "";
|
document.getElementById("graph").innerHTML = "";
|
||||||
|
clear_visualization();
|
||||||
|
model.visualize(initial_state);
|
||||||
|
states = [initial_state];
|
||||||
|
current_state = initial_state;
|
||||||
|
data = {
|
||||||
|
nodes: [{ id: 0 }],
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
graph = null;
|
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
|
// 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
|
document
|
||||||
.getElementById("generate_graph_button")
|
.getElementById("generate_graph_button")
|
||||||
.addEventListener("click", () => {
|
.addEventListener("click", () => {
|
||||||
clear();
|
clear_graph();
|
||||||
data = generate_sample_data();
|
model.generate(states, initial_state, graph, null);
|
||||||
graph = generate_graph(data);
|
reset_graph_view(graph);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("reset_view_button").addEventListener("click", () => {
|
document.getElementById("reset_view_button").addEventListener("click", () => {
|
||||||
reset_graph_view(graph);
|
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