Compare commits

..

3 Commits

Author SHA1 Message Date
94cd28d403 add playback progress bar
Signed-off-by: szdytom <szdytom@qq.com>
2024-01-20 16:16:26 +08:00
475208def0 add analyzer program
Signed-off-by: szdytom <szdytom@qq.com>
2024-01-20 15:49:18 +08:00
893ebef103 extract include
Signed-off-by: szdytom <szdytom@qq.com>
2024-01-20 15:48:57 +08:00
10 changed files with 648 additions and 150 deletions

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
CC=g++
INCLUDE_DIR=include
CFLAGS=-std=c++17 -fopenmp -Wall -O3 -march=native -I $(INCLUDE_DIR)
box-eater: optimizer/box-eater.cpp
$(CC) $(CFLAGS) -o $@ $^
box-breaker: optimizer/box-breaker.cpp
$(CC) $(CFLAGS) -o $@ $^

200
analyzer/index.html Normal file
View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<head>
<title>Visual Pushbox 2024</title>
</head>
<body>
<main>
<div id="board">
<div id="maze"></div>
<div id="target" class="maze-sqr sqr-target"></div>
<div id="player" class="maze-sqr maze-mv sqr-player"></div>
<div id="box" class="maze-sqr maze-mv sqr-box"></div>
</div>
<div id="control-panel">
<div class="ctrl-title">Analyzer</div>
<table class="full-width info-table"><tbody>
<tr><td>Analyze Status:</td><td id="analyze-status" class="text-right">Not Analyzed</td></tr>
<tr><td>Total Steps:</td><td id="analyze-steps" class="text-right">N/A</td></tr>
</tbody></table>
<div class="btn-group full-width top-margin">
<button id="analyze" class="btn float-left full-width">Analyze</button>
</div>
<div class="ctrl-title top-margin">Playback</div>
<div id="progress-indicator">
<div id="progress-value" class="float-left">415 / 1618</div>
<div id="progress-left" class="float-right">-1203</div>
</div>
<div id="progress-bar" class="full-width top-margin">
<div id="progress-bar-fg" class="full-height"></div>
</div>
<div class="ctrl-title top-margin">Import / Export</div>
<textarea autocomplete="off" spellcheck="false" id="maze_charmap"></textarea>
<div id="charmap_controls" class="btn-group full-width top-margin">
<button id="charmap_import" class="btn float-left">Import</button>
<button id="charmap_export" class="btn float-right">Export</button>
</div>
</div>
</main>
</body>
<script type="module" src="index.js"></script>
<style>
#progress-bar {
background-color: lightgrey;
height: 10px;
}
#progress-bar-fg {
width: 30%;
background-color: orange;
}
.full-height {
height: 100%;
}
.full-width {
width: 100%;
}
.info-table, .info-table tr, .info-table td {
border: none;
padding: 0;
outline: none;
border-spacing: 0;
}
.text-right {
text-align: right;
}
main {
margin: 40px;
}
button {
border-radius: 0px;
border: none;
background-color: lightgray;
}
button:hover {
background-color: grey;
cursor: pointer;
}
button[disabled] {
cursor: not-allowed;
}
textarea {
resize: none;
outline: none;
border-radius: 0px;
}
.ctrl-title {
font-weight: bold;
margin-bottom: 10px;
border-bottom: 1px solid black;
font-size: x-large;
width: 100%;
}
#control-panel {
margin-left: 40px;
display: inline-block;
width: 300px;
height: calc(80vh - 20px);
border: 1px solid black;
padding: 10px;
}
.top-margin {
margin-top: 10px;
}
.btn {
height: 30px;
min-width: 80px;
}
.float-left {
display: inline-block;
float: left;
}
.float-right {
display: inline-block;
float: right;
}
#maze_charmap {
width: calc(100% - 2px);
height: 298px;
display: block;
margin: 0;
padding: 0;
overflow: hidden;
font-size: 13.33px;
font-family: monospace;
}
#board {
width: 80vh;
height: 80vh;
border: 1px solid black;
padding: 0;
display: inline-block;
float: left;
position: relative;
}
.maze-sqr {
position: absolute;
width: 4vh;
height: 4vh;
top: 0;
left: 0;
}
#maze {
position: relative;
z-index: 5;
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding: 0;
}
.maze-mv {
z-index: 10;
transition: top 0.2s, left 0.2s;
}
.sqr-wall {
background-color: grey;
}
.sqr-space {
background-color: rgba(0, 0, 0, 0);
}
.sqr-target {
background-color: lime;
}
.sqr-player {
background-color: lightblue;
}
.sqr-box {
background-color: orange;
}
</style>
</html>

138
analyzer/index.js Normal file
View File

@ -0,0 +1,138 @@
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() {
resetAnalyze();
if (current_worker != null) {
current_worker.terminate();
current_worker = null;
analyze_button.innerHTML = 'Analyze';
return;
}
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);
});
});

209
analyzer/pushbox.js Normal file
View File

@ -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;
}
};

43
analyzer/queue.js Normal file
View File

@ -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;
}
};

9
analyzer/solver.js Normal file
View File

@ -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() });
};

38
analyzer/vec2.js Normal file
View File

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

View File

@ -1,167 +1,19 @@
#include <bits/stdc++.h>
#include "pushbox.h"
#include "box-solver.h"
using namespace std;
const int N = 20;
mt19937 rng(random_device{}());
uniform_real_distribution<float> d01(0, 1);
uniform_int_distribution<int> rng10(0, 9);
uniform_int_distribution<int> rngN(0, 19);
enum {
POI_PERSON,
POI_BOX,
POI_TARGET,
POI_EXCEED,
};
#define compiler_assume(condition) \
do { \
if (!(condition)) \
__builtin_unreachable(); \
} while (false); \
struct Maze {
bitset<N> M[N];
int poi[3][2];
int at(int x, int y) const {
return M[x][y];
}
void set(int x, int y, int v) {
M[x][y] = v;
}
void flip(int x, int y) {
M[x].flip(y);
}
bool isValid() const {
for (int i = 0; i < POI_EXCEED; ++i) {
for (int j : {0, 1}) {
if (poi[i][j] < 0 || poi[i][j] >= N)
return false;
}
if (at(poi[i][0], poi[i][1]) == 0)
return false;
}
return true;
}
};
inline Maze loadMaze(std::FILE *f) {
char *s = new char[N + 5];
Maze res;
for (int i = 0; i < N; ++i) {
std::fscanf(f, "%s", s);
for (int j = 0; j < N; ++j) {
res.set(i, j, s[j] != '#');
switch (s[j]) {
case 'P':
res.poi[POI_PERSON][0] = i;
res.poi[POI_PERSON][1] = j;
break;
case '*':
res.poi[POI_BOX][0] = i;
res.poi[POI_BOX][1] = j;
break;
case 'O':
res.poi[POI_TARGET][0] = i;
res.poi[POI_TARGET][1] = j;
break;
}
}
}
delete[] s;
return res;
}
inline void writeMaze(const Maze &m, std::FILE *f) {
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
if (i == m.poi[POI_PERSON][0] && j == m.poi[POI_PERSON][1])
std::fputc('P', f);
else if (i == m.poi[POI_BOX][0] && j == m.poi[POI_BOX][1])
std::fputc('*', f);
else if (i == m.poi[POI_TARGET][0] && j == m.poi[POI_TARGET][1])
std::fputc('O', f);
else if (m.at(i, j) == 0)
std::fputc('#', f);
else
std::fputc('.', f);
}
std::fputc('\n', f);
}
}
namespace Solve {
bitset<N> mp[N];
int dis[N][N][N][N];
queue<tuple<int, int, int, int>> qu;
inline int bfs(int px, int py, int bx, int by, int tx, int ty) {
memset(dis, 127, sizeof(dis));
while (!qu.empty())
qu.pop();
auto expand = [&](int npx, int npy, int nbx, int nby, int d) {
if (npx < 0 || npx >= N)
return;
if (npy < 0 || npy >= N)
return;
if (nbx < 0 || nbx >= N)
return;
if (nby < 0 || nby >= N)
return;
if (mp[npx][npy] == 0)
return;
if (mp[nbx][nby] == 0)
return;
if (npx == nbx && npy == nby)
return;
if (dis[npx][npy][nbx][nby] <= d)
return;
dis[npx][npy][nbx][nby] = d;
qu.emplace(npx, npy, nbx, nby);
};
expand(px, py, bx, by, 0);
while (!qu.empty()) {
auto [npx, npy, nbx, nby] = qu.front();
int d = dis[npx][npy][nbx][nby];
qu.pop();
if (nbx == tx && nby == ty)
return d;
expand(npx + 1, npy, nbx, nby, d + 1);
expand(npx - 1, npy, nbx, nby, d + 1);
expand(npx, npy + 1, nbx, nby, d + 1);
expand(npx, npy - 1, nbx, nby, d + 1);
if (npx + 1 == nbx && npy == nby)
expand(npx + 1, npy, nbx + 1, nby, d + 1);
if (npx - 1 == nbx && npy == nby)
expand(npx - 1, npy, nbx - 1, nby, d + 1);
if (npx == nbx && npy + 1 == nby)
expand(npx, npy + 1, nbx, nby + 1, d + 1);
if (npx == nbx && npy - 1 == nby)
expand(npx, npy - 1, nbx, nby - 1, d + 1);
}
return -1;
}
inline int solve(Maze q) {
for (int i = 0; i < 20; ++i)
mp[i] = q.M[i];
return bfs(q.poi[POI_PERSON][0], q.poi[POI_PERSON][1]
, q.poi[POI_BOX][0], q.poi[POI_BOX][1], q.poi[POI_TARGET][0], q.poi[POI_TARGET][1]);
}
}; // namespace Solve
const float initial_temperature = 10;
const float initial_temperature_pv = 60;
const float termperature_delta = .97;