diff --git a/analyzer/index.html b/analyzer/index.html
new file mode 100644
index 0000000..48e8eda
--- /dev/null
+++ b/analyzer/index.html
@@ -0,0 +1,175 @@
+
+
+
+ Visual Pushbox 2024
+
+
+
+
+
+
+
Analyzer
+
+
+ Analyze Status: | Not Analyzed |
+ Total Steps: | N/A |
+
+
+
+
+
+
Import / Export
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/analyzer/index.js b/analyzer/index.js
new file mode 100644
index 0000000..56687eb
--- /dev/null
+++ b/analyzer/index.js
@@ -0,0 +1,136 @@
+import { Vec2 } from './vec2.js';
+import { N, SQR_TYPE, Maze, AnalyzeContext, State } from './pushbox.js';
+
+class BoardSquare {
+ constructor(attach_element) {
+ this.e = attach_element ?? document.createElement('div');
+ this.e.classList.add('fixtl', 'maze-sqr');
+ this.e.style.top = '0';
+ this.e.style.left = '0';
+ this.type = null;
+ }
+
+ moveTo(x, y) {
+ this.e.style.top = `${4 * x}vh`;
+ this.e.style.left = `${4 * y}vh`;
+ return this;
+ }
+
+ moveToV(v) {
+ return this.moveTo(v.x, v.y);
+ }
+
+ setType(type) {
+ if (this.type) {
+ this.e.classList.remove(`sqr-${this.type}`);
+ }
+ this.type = type;
+ this.e.classList.add(`sqr-${this.type}`);
+ return this;
+ }
+};
+
+
+class BoardUI {
+ constructor() {
+ this.maze = document.getElementById('maze');
+ this.target = new BoardSquare(document.getElementById('target'));
+ this.box = new BoardSquare(document.getElementById('box'));
+ this.player = new BoardSquare(document.getElementById('player'));
+ this.squares = [];
+ }
+
+ drawMaze(maze) {
+ while (this.maze.hasChildNodes()) {
+ this.maze.removeChild(this.maze.lastChild);
+ }
+
+ this.squares = [];
+ for (let i = 0; i < N; ++i) {
+ for (let j = 0; j < N; ++j) {
+ let sqr = new BoardSquare();
+ sqr.moveTo(i, j);
+ if (maze.get(i, j) == SQR_TYPE.WALL) {
+ sqr.setType('wall');
+ } else {
+ sqr.setType('space');
+ }
+ this.maze.appendChild(sqr.e);
+ }
+ }
+ this.target.moveToV(maze.target);
+ }
+
+ updateState(state) {
+ this.player.moveToV(state.player);
+ this.box.moveToV(state.box);
+ }
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ const charmap_input = document.getElementById('maze_charmap');
+ const analyze_button = document.getElementById('analyze');
+ const analyze_status_element = document.getElementById('analyze-status');
+ let board = new BoardUI();
+ let maze = null, analyze_res = null;
+ let current_worker = null;
+
+ function resetAnalyze() {
+ document.getElementById('analyze-status').innerHTML = 'Not Analyzed';
+ document.getElementById('analyze-steps').innerHTML = 'N/A';
+ analyze_res = null;
+ }
+
+ function importMaze(charmap_val) {
+ let charmp = charmap_val.split('\n').map(x => x.trimEnd());
+ maze = Maze.fromCharMap(charmp);
+ board.drawMaze(maze);
+ board.updateState(maze.init);
+ analyze_button.disabled = false;
+ resetAnalyze();
+ }
+
+ if (localStorage.getItem('charmap-cache') != null) {
+ charmap_input.value = localStorage.getItem('charmap-cache');
+ importMaze(charmap_input.value);
+ } else {
+ analyze_button.disabled = true;
+ }
+
+ document.getElementById('charmap_import').addEventListener('click', function() {
+ localStorage.setItem('charmap-cache', charmap_input.value);
+ importMaze(charmap_input.value);
+ });
+
+ analyze_button.addEventListener('click', function() {
+ if (current_worker != null) {
+ current_worker.terminate();
+ current_worker = null;
+ analyze_button.innerHTML = 'Analyze';
+ }
+
+ if (maze == null) {
+ return;
+ }
+
+ current_worker = new Worker('solver.js?v=7', { type: 'module' });
+ analyze_status_element.innerHTML = 'Dispatching';
+ analyze_button.innerHTML = 'Stop';
+ current_worker.onmessage = (msg_r) => {
+ let msg = msg_r.data;
+ if (msg.what === 'started') {
+ analyze_status_element.innerHTML = 'Running';
+ } else if (msg.what == 'done') {
+ analyze_res = msg.value;
+ console.log(analyze_res);
+ analyze_status_element.innerHTML = 'Done';
+ document.getElementById('analyze-steps').innerHTML = analyze_res.step.toString();
+ current_worker.terminate();
+ current_worker = null;
+ analyze_button.innerHTML = 'Analyze';
+ }
+ };
+ current_worker.postMessage(maze);
+ });
+});
+
diff --git a/analyzer/pushbox.js b/analyzer/pushbox.js
new file mode 100644
index 0000000..a557cfa
--- /dev/null
+++ b/analyzer/pushbox.js
@@ -0,0 +1,209 @@
+import { Vec2 } from './vec2.js';
+import { Queue } from './queue.js';
+
+export const N = 20;
+export const BSIZE = new Vec2(N, N);
+
+export const SQR_TYPE = {
+ WALL: 0,
+ SPACE: 1,
+ EXCEED: 2,
+};
+
+export class State {
+ constructor(player, box) {
+ this.player = player ?? new Vec2(0, 0);
+ this.box = box ?? new Vec2(1, 1);
+ }
+
+ asHash() {
+ return this.player.x + this.player.y * N
+ + this.box.x * N * N + this.box.y * N * N * N;
+ }
+
+ isValid() {
+ return !this.player.equals(this.box) && this.player.isInside(Vec2.zero, BSIZE)
+ && this.box.isInside(Vec2.zero, BSIZE);
+ }
+
+ isValidInMaze(maze) {
+ return this.isValid() && maze.getV(this.player) && maze.getV(this.box);
+ }
+
+ clone() {
+ return new State(this.player.clone(), this.box.clone());
+ }
+
+ toString() {
+ return `[State: player ${this.player.toString()}, box ${this.box.toString()}]`;
+ }
+
+ static fromRaw(o) {
+ return new State(Vec2.fromRaw(o.player), Vec2.fromRaw(o.box));
+ }
+}
+
+export class Maze {
+ constructor() {
+ this.mp = new Array(N * N).fill(SQR_TYPE.SPACE);
+ this.target = new Vec2(0, 0);
+ this.init = new State();
+ }
+
+ static fromRaw(o) {
+ let res = new Maze();
+ res.mp = o.mp;
+ res.target = Vec2.fromRaw(o.target);
+ res.init = State.fromRaw(o.init);
+ return res;
+ }
+
+ get(x, y) {
+ return this.mp[x * N + y];
+ }
+
+ getV(v) {
+ return this.get(v.x, v.y);
+ }
+
+ set(x, y, v) {
+ this.mp[x * N + y] = v;
+ return this;
+ }
+
+ setV(p, v) {
+ return this.set(p.x, p.y, v);
+ }
+
+ flip(x, y) {
+ this.mp[x * N + y] ^= 1;
+ return this;
+ }
+
+ flipV(p) {
+ return this.flip(p.x, p.y);
+ }
+
+ exportCharMap(s) {
+ let state = s ?? this.init;
+ let res = new Array(N);
+ for (let i = 0; i < N; ++i) {
+ let line = [];
+ for (let j = 0; j < N; ++j) {
+ let c = '.';
+ if (this.get(i, j) == SQR_TYPE.WALL) {
+ c = '#';
+ }
+
+ if (this.target.match(i, j)) {
+ c = 'O';
+ } else if (state.player.match(i, j)) {
+ c = 'P';
+ } else if (state.target.match(i, j)) {
+ c = '*';
+ }
+ line.push(c);
+ }
+ res[i] = line.join('');
+ }
+ return res;
+ }
+
+ static fromCharMap(s) {
+ let res = new Maze();
+ for (let i = 0; i < N; ++i) {
+ for (let j = 0; j < N; ++j) {
+ if (s[i][j] == '#') {
+ res.set(i, j, SQR_TYPE.WALL);
+ } else {
+ res.set(i, j, SQR_TYPE.SPACE);
+ }
+
+ if (s[i][j] == 'O') {
+ res.target = new Vec2(i, j);
+ } else if (s[i][j] == 'P') {
+ res.init.player = new Vec2(i, j);
+ } else if (s[i][j] == '*') {
+ res.init.box = new Vec2(i, j);
+ }
+ }
+ }
+ return res;
+ }
+};
+
+export class AnalyzeContext {
+ constructor(maze) {
+ this.maze = maze;
+ this.dis = new Array(N * N * N * N).fill(-1);
+ this.source = new Array(N * N * N * N).fill(null);
+ this.dis[maze.init.asHash()] = 0;
+
+ this.is_bfs_done = false;
+ this.step = -1;
+ this.path = null;
+ }
+
+ asResult() {
+ return {
+ is_bfs_done: this.is_bfs_done,
+ dis: this.dis,
+ source: this.source,
+ step: this.step,
+ path: this.path,
+ };
+ }
+
+ bfs() {
+ const dt = [new Vec2(0, 1), new Vec2(0, -1), new Vec2(1, 0), new Vec2(-1, 0)];
+
+ if (this.is_bfs_done) {
+ return this.step;
+ }
+
+ let end_state = null;
+ let q = new Queue();
+ q.push(this.maze.init.clone());
+ while (q.size()) {
+ let x = q.front();
+ q.pop();
+ console.log(x.toString());
+
+ let dis = this.dis[x.asHash()];
+ if (x.box.equals(this.maze.target) && this.step == -1) {
+ this.step = dis;
+ end_state = x;
+ }
+
+ for (let d of dt) {
+ let y = x.clone();
+ y.player.addTo(d);
+ if (y.player.equals(y.box)) {
+ y.box.addTo(d);
+ }
+
+ let yh = y.asHash();
+ if (y.isValidInMaze(this.maze) && this.dis[yh] == -1) {
+ console.log(y.toString());
+ this.dis[yh] = dis + 1;
+ this.source[yh] = x.clone();
+ q.push(y);
+ }
+ }
+ }
+ this.is_bfs_done = true;
+
+ if (this.step != -1) {
+ this.path = [];
+ let p = end_state;
+ while (p != null) {
+ this.path.push(p);
+ p = this.source[p.asHash()];
+ }
+ this.path.reverse();
+ }
+
+ return this.step;
+ }
+};
+
diff --git a/analyzer/queue.js b/analyzer/queue.js
new file mode 100644
index 0000000..a7cbec6
--- /dev/null
+++ b/analyzer/queue.js
@@ -0,0 +1,43 @@
+export class Queue {
+ constructor() {
+ this.val = [];
+ this.ptr = 0;
+ }
+
+ size() {
+ return this.val.length - this.ptr;
+ }
+
+ get length() {
+ return this.size();
+ }
+
+ empty() {
+ return this.size() == 0;
+ }
+
+ front() {
+ return this.val[this.ptr];
+ }
+
+ _rebuild() {
+ if (this.ptr > 0) {
+ this.val = this.val.slice(this.ptr);
+ this.ptr = 0;
+ }
+ }
+
+ pop() {
+ this.ptr += 1;
+ if (this.ptr >= 16 && this.ptr >= this.val.length / 2) {
+ this._rebuild();
+ }
+ return this;
+ }
+
+ push(x) {
+ this.val.push(x);
+ return this;
+ }
+};
+
diff --git a/analyzer/solver.js b/analyzer/solver.js
new file mode 100644
index 0000000..52b4e35
--- /dev/null
+++ b/analyzer/solver.js
@@ -0,0 +1,9 @@
+import { Maze, AnalyzeContext } from './pushbox.js';
+
+onmessage = (msg) => {
+ let ac = new AnalyzeContext(Maze.fromRaw(msg.data));
+ postMessage({ what: 'started' });
+ ac.bfs();
+ postMessage({ what: 'done', value: ac.asResult() });
+};
+
diff --git a/analyzer/vec2.js b/analyzer/vec2.js
new file mode 100644
index 0000000..2848b08
--- /dev/null
+++ b/analyzer/vec2.js
@@ -0,0 +1,38 @@
+export class Vec2 {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ addTo(o) {
+ this.x += o.x;
+ this.y += o.y;
+ }
+
+ isInside(lt, rb) {
+ return this.x >= lt.x && this.x < rb.x && this.y >= lt.y && this.y < rb.y;
+ }
+
+ clone() {
+ return new Vec2(this.x, this.y);
+ }
+
+ equals(o) {
+ return this.x == o.x && this.y == o.y;
+ }
+
+ match(x, y) {
+ return this.x == x && this.y == y;
+ }
+
+ toString() {
+ return `(${this.x}, ${this.y})`;
+ }
+
+ static fromRaw(o) {
+ return new Vec2(o.x, o.y);
+ }
+
+ static zero = new Vec2(0, 0);
+};
+