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); +}; +