import { Game, NORTH, SolveResult, WEST, quarters_for_color } from "../wasm/ricochetrobots.js";
import BoardTextDialog from "./BoardTextDialog.js";
import CellDialog from "./CellDialog.js";
import Dialog from "./Dialog.js";
import GameUI, { GameUIEventType } from "./GameUI.js";
import { getGame, setGame } from "./params.js";
import { clamp, exhaustive } from "./util.js";
function startingGame(): Game {
let g = getGame();
return g ? Game.from_compact(g) : Game.default();
}
const CORNERS = [ "Top Left", "Top Right", "Bottom Right", "Bottom Left" ];
export default class MainUI {
private nav = document.createElement("nav");
private gameUI = new GameUI(async e => {
switch (e.type) {
case GameUIEventType.CELL_CLICK: {
let [type, ...args] = await new CellDialog().result();
switch (type) {
case "mirror": {
let [color, lean] = args;
if (color == "none") {
this.game.clear_mirror(e.pos.x, e.pos.y);
} else {
this.game.set_mirror(e.pos.x, e.pos.y, lean == "left", +color);
}
this.updateFromGame();
break;
}
case "target": {
let [c] = args;
let color = c == "any" ? undefined : +c;
this.game.set_target(e.pos.x, e.pos.y, color);
this.updateFromGame();
break;
}
default:
console.error("Unexpected type", type);
}
break;
}
case GameUIEventType.ROBOT_DRAG_START: {
this.stopAnimation();
break;
}
case GameUIEventType.ROBOT_DROP: {
this.game.set_robot(e.robot, e.pos.x, e.pos.y);
this.updateFromGame();
break;
}
case GameUIEventType.WALL_CLICK: {
this.game.set_wall(
e.pos.x, e.pos.y,
e.isHorizontal ? NORTH() : WEST(),
!e.present);
this.updateFromGame();
break;
}
default:
exhaustive(e);
}
});
private output = document.createElement("div");
private game = startingGame();
private currentSolution?: SolveResult;
private currentStep = 0;
private animation: unknown;
constructor() {
this.updateFromGame();
this.createNavButton("Solve", () => {
this.solve();
});
this.nav.insertAdjacentHTML("beforeend", "
");
this.createNavButton("Stock Tiles", () => {
this.selectStockTiles();
});
this.createNavButton("⇅", async () => {
let d = new BoardTextDialog(this.game.serialize(), this.game.to_driftingdroids());
this.setGame(await d.result());
}, { title: "Import + Export this Puzzle" });
const loading = document.getElementById("loading");
document.body.insertBefore(this.nav, loading)
document.body.insertBefore(this.gameUI.root, loading)
document.body.insertBefore(this.output, loading)
loading?.remove();
}
createNavButton(
name: string,
action: () => void,
{title}: {title?: string} = {},
) {
let button = document.createElement("button");
if (title) {
button.title = title;
}
button.textContent = name;
button.onclick = action;
this.nav.append(button);
}
setGame(newGame: Game, commit=true) {
if (this.game) this.game.free();
this.game = newGame;
this.updateFromGame(commit);
}
updateFromGame(commit=true) {
if (!this.game) {
return;
}
this.gameUI.update(this.game);
if (commit) {
setGame(this.game.to_compact());
}
}
showStep(i: number) {
if (!this.currentSolution) {
throw new Error("No current solution.");
}
let sequential = i == this.currentStep + 1;
this.currentStep = i;
if (i == 0) {
this.setGame(this.currentSolution.start(), false);
return
}
let step = this.currentSolution.step(i - 1);
if (!this.animation && !sequential) {
this.setGame(step.resulting_game(), false);
return;
}
this.setGame(step.source_game(), false);
let robot = document.getElementById("robot-" + step.robot())!;
robot.setAttribute("cx", `${0.5}`);
robot.setAttribute("cy", `${0.5}`);
let trace = step.trace();
trace.unshift(this.game.robot(step.robot()));
let keyframes = trace.map(pos => ({
transform: `translate(${pos.x}px, ${pos.y}px)`,
}));
let cssAnimation = robot.animate(keyframes, {
duration: (trace.length - 1) * 400,
// endDelay: 2, // milliseconds on Firefox, seconds on chrome.
easing: "cubic-bezier(0.2, 0, 0.8, 1)",
fill: "both",
});
step.free();
let currentAnimation = this.animation;
if (!currentAnimation) return;
cssAnimation.onfinish = e => {
if (this.animation != currentAnimation) return;
if (!this.currentSolution) return;
if (i < this.currentSolution.len()) {
this.showStep(i + 1);
} else {
setTimeout(() => {
if (this.animation != currentAnimation) return;
this.showStep(1);
}, 1000);
}
};
}
startAnimation() {
this.animation = {};
this.showStep(1);
}
stopAnimation() {
this.animation = undefined;
}
async selectStockTiles() {
let colors = [1, 2, 3, 4];
for (let quadrant = 0; quadrant < 4; quadrant++) {
class ColorDialog extends Dialog {
render() {
this.body.innerHTML = `Select the colour of the ${CORNERS[quadrant]} tile.
`
colors.forEach((c, i) => {
let el = document.createElement("button");
el.className = `color c-${c}`;
el.value = `${i}`;
this.body.appendChild(el);
});
this.renderCancel();
}
extract(button: string): number {
return +button;
}
}
let colorIndex = await new ColorDialog().result();
let color = colors.splice(colorIndex, 1)[0]!;
class QuarterDialog extends Dialog {
render() {
this.body.innerHTML = `Select ${CORNERS[quadrant]} tile.
`;
let quarters = quarters_for_color(color);
for (let i = 0; i < quarters.len(); i++) {
let el = document.createElement("button");
el.className = "quarter";
el.innerHTML = quarters.get(quadrant, i);
el.value = `${i}`;
this.body.appendChild(el);
}
}
extract(button: string): number {
return +button;
}
}
let quarterIndex = await new QuarterDialog().result();
this.game.set_quadrant(quadrant, color, quarterIndex);
}
this.updateFromGame();
}
solve() {
this.stopAnimation();
if (this.currentSolution) {
this.currentSolution.free();
this.currentSolution = undefined;
}
let start = Date.now();
let result = "Failed";
let steps = 0;
try {
this.currentSolution = this.game.solve();
steps = this.currentSolution.len();
this.startAnimation();
let buttons = `
`;
for (let i = 0; i <= steps; i++) {
buttons += ``
}
this.output.innerHTML = buttons;
this.output.addEventListener("click", e => {
let target = e.target as HTMLElement;
if (target.tagName != "BUTTON") return;
let id = target.id;
if (!this.currentSolution) {
return;
}
if (id == "auto") {
this.startAnimation();
return;
} else {
this.stopAnimation();
}
if (id == "prev") {
this.showStep(Math.max(this.currentStep - 1, 0));
} else if (id == "next") {
this.showStep(Math.min(this.currentStep + 1, this.currentSolution.len()));
} else {
this.showStep(+id);
}
});
result = "Solved";
} catch (e) {
console.error(e);
this.output.textContent = `Error: ${e}`;
}
let ms = Date.now() - start;
let time = document.createElement("p");
time.textContent += `\n${result} in ${ms/1000}s.`;
this.output.append(time);
}
}