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