| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { CENTER, COLORS, FLOOR, node } from "./dom.js"; |
|
|
| const TAGS = { move: "", round: "round", end: "final", think: "search", start: "setup" }; |
|
|
| |
| const shortSource = (source) => (source === CENTER ? "middle" : "factory " + (source + 1)); |
| const shortDest = (dest) => (dest === FLOOR ? "floor" : "row " + (dest + 1)); |
|
|
| |
| export function glyph(color) { |
| const g = node("i", "glyph"); |
| g.dataset.color = color; |
| g.title = COLORS[color]; |
| return g; |
| } |
|
|
| |
| export function glyphRun(color, count, cls) { |
| const wrap = node("span", "glyphs" + (cls ? " " + cls : "")); |
| for (let i = 0; i < count; i++) wrap.appendChild(glyph(color)); |
| return wrap; |
| } |
|
|
| |
| export function grade(delta) { |
| if (delta === null || delta === undefined) return "unrated"; |
| if (delta >= -0.005) return "best"; |
| if (delta >= -0.05) return "slip"; |
| return "blunder"; |
| } |
|
|
| export function formatDelta(delta) { |
| if (delta === null || delta === undefined) return "unrated"; |
| const rounded = Math.abs(delta) < 0.005 ? 0 : delta; |
| return (rounded === 0 ? "0.00" : rounded.toFixed(2)).replace("-", "−"); |
| } |
|
|
| |
| export function coachChip(coach) { |
| const chip = node("span", "coach-chip"); |
| if (!coach) return chip; |
| if (coach.pending) { |
| chip.dataset.grade = "pending"; |
| chip.appendChild(node("span", "coach-delta", "rating…")); |
| return chip; |
| } |
| if (coach.unrated) { |
| chip.dataset.grade = "unrated"; |
| chip.appendChild(node("span", "coach-delta", "unrated")); |
| chip.appendChild( |
| node("span", "coach-why", coach.reason || "the search never explored this move") |
| ); |
| return chip; |
| } |
| chip.dataset.grade = grade(coach.delta); |
| chip.appendChild(node("span", "coach-delta", formatDelta(coach.delta))); |
| if (coach.best_text && coach.delta <= -0.02) { |
| chip.appendChild(node("span", "coach-why", "AI preferred: " + coach.best_text)); |
| } else if (coach.forced) { |
| chip.appendChild(node("span", "coach-why", "only move")); |
| } |
| return chip; |
| } |
|
|
| |
| function isPictorial(entry) { |
| return ( |
| entry.kind === "move" && |
| typeof entry.color === "number" && |
| typeof entry.count === "number" && |
| typeof entry.source === "number" && |
| typeof entry.dest === "number" |
| ); |
| } |
|
|
| |
| function moveBody(entry) { |
| const body = node("span", "log-text"); |
| body.appendChild(glyphRun(entry.color, entry.count)); |
| if (entry.took_marker) { |
| const wrap = node("span", "glyphs"); |
| const chip = node("i", "glyph marker-glyph", "1"); |
| chip.title = "and the first-player marker"; |
| wrap.appendChild(chip); |
| body.appendChild(wrap); |
| } |
| body.appendChild(node("span", "log-where", shortSource(entry.source))); |
| body.appendChild(node("span", "log-arrow", "→")); |
| body.appendChild(node("span", "log-where", shortDest(entry.dest))); |
| if (entry.overflow) { |
| body.appendChild( |
| node("span", "log-aside", " · " + entry.overflow + " to the floor") |
| ); |
| } |
| body.title = entry.text || ""; |
| return body; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function renderLog(host, entries) { |
| host.innerHTML = ""; |
| const list = (entries || []).slice().reverse(); |
| list.forEach((entry) => { |
| const li = node("li", "log-entry"); |
| li.dataset.kind = entry.kind; |
| if (entry.side) li.dataset.side = entry.side; |
| if (entry.ply !== undefined) li.dataset.ply = entry.ply; |
| const tag = entry.kind === "move" ? entry.side || "" : TAGS[entry.kind] || entry.kind; |
| li.appendChild(node("span", "log-tag", tag === "human" ? "you" : tag)); |
| const body = isPictorial(entry) ? moveBody(entry) : node("span", "log-text", entry.text); |
| if (entry.coach) body.appendChild(coachChip(entry.coach)); |
| li.appendChild(body); |
| host.appendChild(li); |
| }); |
| host.scrollTop = 0; |
| } |
|
|