补上浏览器试玩客户端,只渲染状态并提交合法行动。
占位美术先顶着,方便点选验证而不把规则写进界面。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
:root {
|
||||
--ink: #1c1814;
|
||||
--paper: #efe6d6;
|
||||
--felt: #2c3a32;
|
||||
--felt-2: #24312a;
|
||||
--line: #c9b89a;
|
||||
--muted: #6d6356;
|
||||
--accent: #8b3a2a;
|
||||
--ok: #2f5d3a;
|
||||
--focus: #c4a35a;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body {
|
||||
font: 13px/1.4 "Segoe UI", "PingFang SC", sans-serif;
|
||||
color: var(--ink);
|
||||
background: var(--felt-2);
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.bar {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 14px;
|
||||
background: var(--paper);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.bar-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.muted { color: var(--muted); margin-left: 8px; font-size: 12px; }
|
||||
.seed { font-size: 12px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
|
||||
input, button {
|
||||
font: inherit;
|
||||
border: 1px solid #b6a78c;
|
||||
background: #fffdf8;
|
||||
color: var(--ink);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
button { cursor: pointer; }
|
||||
button:hover { background: #f3ead8; }
|
||||
button:focus-visible, input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
||||
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.banner {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
padding: 6px 14px;
|
||||
background: #f3d2c6;
|
||||
color: #6a2418;
|
||||
}
|
||||
|
||||
.select-strip {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 8px 14px;
|
||||
background: #2a2218;
|
||||
color: #f4e7c8;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
font-size: 13px;
|
||||
}
|
||||
.select-strip button {
|
||||
background: #4a3424;
|
||||
color: #f4e7c8;
|
||||
border-color: #7a5a3a;
|
||||
}
|
||||
|
||||
.table {
|
||||
padding: 12px 16px 20px;
|
||||
background:
|
||||
radial-gradient(ellipse at top, #355043 0%, transparent 55%),
|
||||
var(--felt);
|
||||
}
|
||||
|
||||
.rail {
|
||||
background: var(--paper);
|
||||
border-left: 1px solid var(--line);
|
||||
padding: 10px 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #d8cbb3;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rail h2 { color: var(--muted); margin-top: 12px; }
|
||||
.rail h2:first-child { margin-top: 0; }
|
||||
|
||||
.row { margin-bottom: 10px; }
|
||||
.lane {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-height: 96px;
|
||||
padding: 8px;
|
||||
border: 1px dashed rgba(232, 220, 196, 0.35);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--paper);
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.hero.you { justify-content: flex-start; }
|
||||
.hero.foe { cursor: default; }
|
||||
.hero.foe.targetable {
|
||||
border-color: var(--focus);
|
||||
background: rgba(196, 163, 90, 0.18);
|
||||
cursor: pointer;
|
||||
}
|
||||
.hero.foe.target-picked {
|
||||
border-color: var(--accent);
|
||||
background: rgba(139, 58, 42, 0.28);
|
||||
}
|
||||
.hero-tag {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #2a2218;
|
||||
background: var(--focus);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.portrait svg { width: 40px; height: 40px; display: block; }
|
||||
.who { font-size: 11px; opacity: 0.8; }
|
||||
.nums { font-variant-numeric: tabular-nums; font-size: 16px; font-weight: 650; }
|
||||
|
||||
.end {
|
||||
margin-left: auto;
|
||||
background: var(--accent);
|
||||
color: #fffdf8;
|
||||
border-color: #6e2c20;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
.end:hover { background: #a34432; color: #fffdf8; }
|
||||
|
||||
.card {
|
||||
width: 92px;
|
||||
min-height: 118px;
|
||||
padding: 6px;
|
||||
border: 1px solid #5b4a38;
|
||||
border-radius: 6px;
|
||||
background:
|
||||
linear-gradient(#fffaf0, #e8dcc4);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
}
|
||||
.card button-reset { all: unset; }
|
||||
button.card { cursor: pointer; }
|
||||
button.card:hover { border-color: var(--ok); }
|
||||
.card.playable, .card.can-attack, .card.can-activate { outline: 2px solid var(--ok); }
|
||||
.card.selected {
|
||||
outline: 3px solid var(--accent);
|
||||
transform: translateY(-4px);
|
||||
background: linear-gradient(#fff6e0, #f0d8a8);
|
||||
position: relative;
|
||||
}
|
||||
.card.selected::after {
|
||||
content: "选中";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--accent);
|
||||
color: #fffdf8;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.card.drop {
|
||||
outline: 3px dashed var(--focus);
|
||||
background: linear-gradient(#fffceb, #e8d48a);
|
||||
position: relative;
|
||||
}
|
||||
.card.drop::before {
|
||||
content: "目标";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 4px;
|
||||
background: var(--focus);
|
||||
color: #2a2218;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.card.system { background: linear-gradient(#e8efe6, #cfd8c8); }
|
||||
.face { font-size: 26px; line-height: 1.1; }
|
||||
.name { font-size: 11px; font-weight: 650; }
|
||||
.meta { font-size: 10px; color: var(--muted); }
|
||||
.stats { font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.ai-trace { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
|
||||
.ai-step {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
background: #fffdf8;
|
||||
}
|
||||
.ai-step strong { display: block; margin-bottom: 4px; }
|
||||
.ai-cands { margin: 0; padding-left: 16px; font-size: 12px; color: #3f3830; }
|
||||
.ai-cands li.chosen { font-weight: 700; color: var(--accent); }
|
||||
.ai-cands .score { color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.empty {
|
||||
color: rgba(232, 220, 196, 0.55);
|
||||
font-size: 12px;
|
||||
padding: 20px 8px;
|
||||
}
|
||||
|
||||
.actions { display: flex; flex-direction: column; gap: 4px; }
|
||||
.actions button {
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.actions button[data-kind="end"] { background: #f6e4dc; }
|
||||
.hint { color: var(--muted); font-size: 12px; margin: 0 0 8px; }
|
||||
.log {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 12px;
|
||||
color: #3f3830;
|
||||
}
|
||||
.log li { margin: 2px 0; }
|
||||
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
max-width: 280px;
|
||||
min-width: 180px;
|
||||
padding: 10px 12px;
|
||||
background: #1f1a14;
|
||||
color: #f4e7c8;
|
||||
border: 1px solid #c4a35a;
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.tooltip-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.tooltip-title .emoji { font-size: 18px; }
|
||||
.tooltip-holder {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: #c4a35a;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tooltip ul {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.tooltip li { margin: 2px 0; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
.rail { border-left: 0; border-top: 1px solid var(--line); }
|
||||
.end { margin-left: 0; }
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let view = null;
|
||||
/** @type {{ id: number, name: string, kind: string } | null} */
|
||||
let selected = null;
|
||||
|
||||
async function api(path, body) {
|
||||
const res = await fetch(path, {
|
||||
method: body ? "POST" : "GET",
|
||||
headers: body ? { "Content-Type": "application/json" } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("请求失败 " + res.status);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function findCard(id) {
|
||||
if (!view) return null;
|
||||
return (
|
||||
view.you.hand.find((c) => c.id === id) ||
|
||||
view.yourBoard.find((c) => c.id === id) ||
|
||||
view.foeBoard.find((c) => c.id === id) ||
|
||||
view.foe.hand.find((c) => c.id === id) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function sourceActions(id) {
|
||||
return (view?.legal || []).filter((a) => a.sourceId === id);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selected = null;
|
||||
paintSelectionChrome();
|
||||
}
|
||||
|
||||
function paintSelectionChrome() {
|
||||
const strip = $("select-strip");
|
||||
const foe = $("foe-hero");
|
||||
const tag = $("foe-tag");
|
||||
if (!selected || !view) {
|
||||
strip.hidden = true;
|
||||
foe.classList.remove("targetable", "target-picked");
|
||||
tag.hidden = true;
|
||||
document.querySelectorAll(".card").forEach((el) => {
|
||||
el.classList.remove("selected", "drop");
|
||||
});
|
||||
$("hint").textContent = view?.over
|
||||
? "对局结束。开一局新的。"
|
||||
: "点能出的手牌,或点能攻/激活的场面;需要目标时会标出「选中」「目标」。";
|
||||
return;
|
||||
}
|
||||
|
||||
const card = findCard(selected.id);
|
||||
const acts = sourceActions(selected.id);
|
||||
const needsTarget = acts.some(
|
||||
(a) => a.targetEntityId != null || a.targetPlayer != null || a.target2EntityId != null
|
||||
);
|
||||
const heroOk = acts.some((a) => a.targetPlayer === 1);
|
||||
|
||||
strip.hidden = false;
|
||||
$("select-text").textContent = needsTarget
|
||||
? `已选中:${card?.emoji || ""} ${card?.name || "#" + selected.id}(${selected.kind})→ 请再点黄色「目标」牌或敌方主战者`
|
||||
: `已选中:${card?.emoji || ""} ${card?.name || "#" + selected.id}`;
|
||||
$("hint").textContent = needsTarget
|
||||
? "黄框=可选目标。点目标完成行动;点「取消选择」或再点选中牌可取消。"
|
||||
: "该牌无需目标,应已自动执行。";
|
||||
|
||||
foe.classList.toggle("targetable", heroOk);
|
||||
tag.hidden = !heroOk;
|
||||
|
||||
document.querySelectorAll(".card").forEach((el) => {
|
||||
const id = Number(el.dataset.id);
|
||||
el.classList.toggle("selected", id === selected.id);
|
||||
const ok = acts.some((a) => a.targetEntityId === id || a.target2EntityId === id);
|
||||
el.classList.toggle("drop", ok);
|
||||
});
|
||||
}
|
||||
|
||||
function render(v) {
|
||||
view = v;
|
||||
selected = null;
|
||||
$("meta").textContent = `种子 ${v.seed} · 回合 ${v.turn} · ${v.over ? "已结束" : (v.active === 0 ? "你的回合" : "敌方回合")}`;
|
||||
$("banner").hidden = !v.error && !v.over;
|
||||
$("banner").textContent = v.error || (v.over ? `胜者 P${v.winner}` : "");
|
||||
$("foe-nums").textContent = `${v.foe.life} 命 · ${v.foe.energy} 费 · 库${v.foe.deck} 墓${v.foe.graveyard}`;
|
||||
$("you-nums").textContent = `${v.you.life} 命 · ${v.you.energy} 费 · 库${v.you.deck} 墓${v.you.graveyard}`;
|
||||
paintLane("foe-hand", v.foe.hand, false);
|
||||
paintLane("foe-board", v.foeBoard, true);
|
||||
paintLane("you-board", v.yourBoard, true);
|
||||
paintLane("you-hand", v.you.hand, false);
|
||||
paintActions(v.legal);
|
||||
paintAi(v.aiTrace || []);
|
||||
const log = $("log");
|
||||
log.replaceChildren();
|
||||
for (const line of v.log.slice().reverse()) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = line;
|
||||
log.append(li);
|
||||
}
|
||||
$("end").disabled = v.over || !v.legal.some((a) => a.kind === "end");
|
||||
paintSelectionChrome();
|
||||
}
|
||||
|
||||
function paintAi(steps) {
|
||||
const root = $("ai-trace");
|
||||
root.replaceChildren();
|
||||
if (!steps.length) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "hint";
|
||||
p.textContent = "本回合还没有敌方行动记录。";
|
||||
root.append(p);
|
||||
return;
|
||||
}
|
||||
steps.forEach((step, i) => {
|
||||
const box = document.createElement("div");
|
||||
box.className = "ai-step";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `步骤 ${i + 1}:${step.chosenLabel} (${step.chosenScore})`;
|
||||
const ul = document.createElement("ol");
|
||||
ul.className = "ai-cands";
|
||||
for (const c of step.topCandidates) {
|
||||
const li = document.createElement("li");
|
||||
if (c.chosen) li.className = "chosen";
|
||||
li.innerHTML = `${c.label} <span class="score">${c.score}</span>`;
|
||||
ul.append(li);
|
||||
}
|
||||
box.append(title, ul);
|
||||
root.append(box);
|
||||
});
|
||||
}
|
||||
|
||||
function paintLane(id, cards, board) {
|
||||
const root = $(id);
|
||||
root.replaceChildren();
|
||||
if (!cards.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "empty";
|
||||
empty.textContent = "空";
|
||||
root.append(empty);
|
||||
return;
|
||||
}
|
||||
for (const c of cards) {
|
||||
root.append(cardEl(c, board));
|
||||
}
|
||||
}
|
||||
|
||||
function cardEl(c, board) {
|
||||
const el = document.createElement("button");
|
||||
el.type = "button";
|
||||
el.className = "card";
|
||||
if (c.isSystem) el.classList.add("system");
|
||||
if (c.playable) el.classList.add("playable");
|
||||
if (c.canAttack) el.classList.add("can-attack");
|
||||
if (c.canActivate) el.classList.add("can-activate");
|
||||
el.dataset.id = String(c.id);
|
||||
const cd = c.type === "FieldCard" ? ` CD${c.countdown}` : "";
|
||||
const body = c.type === "Creature" ? `${c.attack}/${c.health}` : c.type === "FieldCard" ? "场域" : "法术";
|
||||
el.innerHTML = `<div class="face">${c.emoji}</div>
|
||||
<div class="name">${c.name}</div>
|
||||
<div class="stats">${c.cost}费 ${body}${cd}</div>
|
||||
<div class="meta">${c.holder}${board && c.canAttack ? " ·可攻" : ""}${board && c.canActivate ? " ·可激活" : ""}</div>`;
|
||||
el.addEventListener("click", () => onCard(c));
|
||||
el.addEventListener("pointerenter", (ev) => showTooltip(c, ev));
|
||||
el.addEventListener("pointermove", (ev) => moveTooltip(ev));
|
||||
el.addEventListener("pointerleave", hideTooltip);
|
||||
return el;
|
||||
}
|
||||
|
||||
function showTooltip(c, ev) {
|
||||
const tip = $("tooltip");
|
||||
const lines = (c.detail && c.detail.length ? c.detail : ["无额外效果。"])
|
||||
.map((line) => `<li>${escapeHtml(line)}</li>`)
|
||||
.join("");
|
||||
tip.innerHTML = `<div class="tooltip-title"><span class="emoji">${c.emoji}</span><span>${escapeHtml(c.name)}</span><span class="tooltip-holder">${escapeHtml(c.holder)}</span></div><ul>${lines}</ul>`;
|
||||
tip.hidden = false;
|
||||
moveTooltip(ev);
|
||||
}
|
||||
|
||||
function moveTooltip(ev) {
|
||||
const tip = $("tooltip");
|
||||
if (tip.hidden) return;
|
||||
const pad = 14;
|
||||
const tw = tip.offsetWidth || 220;
|
||||
const th = tip.offsetHeight || 120;
|
||||
let x = ev.clientX + pad;
|
||||
let y = ev.clientY + pad;
|
||||
if (x + tw > window.innerWidth - 8) x = ev.clientX - tw - pad;
|
||||
if (y + th > window.innerHeight - 8) y = ev.clientY - th - pad;
|
||||
tip.style.left = `${Math.max(8, x)}px`;
|
||||
tip.style.top = `${Math.max(8, y)}px`;
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
$("tooltip").hidden = true;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function paintActions(legal) {
|
||||
const root = $("actions");
|
||||
root.replaceChildren();
|
||||
if (!legal.length) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "hint";
|
||||
p.textContent = "没有合法行动。";
|
||||
root.append(p);
|
||||
return;
|
||||
}
|
||||
for (const a of legal) {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.textContent = a.label;
|
||||
b.dataset.kind = a.kind;
|
||||
b.addEventListener("click", () => {
|
||||
clearSelection();
|
||||
act(a.index);
|
||||
});
|
||||
root.append(b);
|
||||
}
|
||||
}
|
||||
|
||||
function onCard(c) {
|
||||
if (!view || view.over) return;
|
||||
const legal = view.legal;
|
||||
|
||||
if (selected && selected.id === c.id) {
|
||||
clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
const hit = legal.find((a) =>
|
||||
a.sourceId === selected.id && (a.targetEntityId === c.id || a.target2EntityId === c.id));
|
||||
if (hit) {
|
||||
clearSelection();
|
||||
act(hit.index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sourced = sourceActions(c.id);
|
||||
if (!sourced.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourced.length === 1 && sourced[0].targetEntityId == null && sourced[0].targetPlayer == null && sourced[0].target2EntityId == null) {
|
||||
clearSelection();
|
||||
act(sourced[0].index);
|
||||
return;
|
||||
}
|
||||
|
||||
const kind = sourced[0].kind === "play" ? "打出" : sourced[0].kind === "attack" ? "攻击" : "激活";
|
||||
selected = { id: c.id, name: c.name, kind };
|
||||
paintSelectionChrome();
|
||||
}
|
||||
|
||||
document.getElementById("foe-hero").addEventListener("click", () => {
|
||||
if (!view || selected == null) return;
|
||||
const hit = view.legal.find((a) => a.sourceId === selected.id && a.targetPlayer === 1);
|
||||
if (hit) {
|
||||
clearSelection();
|
||||
act(hit.index);
|
||||
}
|
||||
});
|
||||
|
||||
$("cancel-select").addEventListener("click", clearSelection);
|
||||
|
||||
async function act(index) {
|
||||
try {
|
||||
render(await api("/api/act", { index }));
|
||||
} catch (e) {
|
||||
$("banner").hidden = false;
|
||||
$("banner").textContent = String(e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
$("new").addEventListener("click", async () => {
|
||||
const raw = $("seed").value.trim();
|
||||
const seed = raw ? Number(raw) : null;
|
||||
render(await api("/api/new", { seed }));
|
||||
});
|
||||
|
||||
$("end").addEventListener("click", () => {
|
||||
const end = view?.legal.find((a) => a.kind === "end");
|
||||
if (end) {
|
||||
clearSelection();
|
||||
act(end.index);
|
||||
}
|
||||
});
|
||||
|
||||
api("/api/state").then(render).catch((e) => {
|
||||
$("banner").hidden = false;
|
||||
$("banner").textContent = String(e.message || e);
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>对战测试 · Card Roguelike</title>
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<header class="bar">
|
||||
<div>
|
||||
<strong>对战测试</strong>
|
||||
<span class="muted" id="meta">载入中…</span>
|
||||
</div>
|
||||
<div class="bar-actions">
|
||||
<label class="seed">种子
|
||||
<input id="seed" type="text" inputmode="numeric" placeholder="随机" />
|
||||
</label>
|
||||
<button type="button" id="new">新对局</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="banner" id="banner" hidden></p>
|
||||
<div class="select-strip" id="select-strip" hidden>
|
||||
<span id="select-text"></span>
|
||||
<button type="button" id="cancel-select">取消选择</button>
|
||||
</div>
|
||||
|
||||
<main class="table">
|
||||
<section class="row" aria-label="敌方手牌">
|
||||
<h2>敌方手牌(测试可见)</h2>
|
||||
<div class="lane hand" id="foe-hand"></div>
|
||||
</section>
|
||||
|
||||
<section class="hero foe" id="foe-hero" data-seat="1" tabindex="0">
|
||||
<div class="portrait" aria-hidden="true">
|
||||
<svg viewBox="0 0 48 48"><circle cx="24" cy="18" r="10" fill="#4a3f35"/><ellipse cx="24" cy="40" rx="16" ry="10" fill="#3a322c"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="who">敌方主战者</div>
|
||||
<div class="nums" id="foe-nums"></div>
|
||||
</div>
|
||||
<span class="hero-tag" id="foe-tag" hidden>可选目标</span>
|
||||
</section>
|
||||
|
||||
<section class="row" aria-label="敌方场面">
|
||||
<h2>敌方场面</h2>
|
||||
<div class="lane" id="foe-board"></div>
|
||||
</section>
|
||||
|
||||
<section class="row" aria-label="己方场面">
|
||||
<h2>己方场面</h2>
|
||||
<div class="lane" id="you-board"></div>
|
||||
</section>
|
||||
|
||||
<section class="hero you" id="you-hero" data-seat="0">
|
||||
<div class="portrait" aria-hidden="true">
|
||||
<svg viewBox="0 0 48 48"><circle cx="24" cy="18" r="10" fill="#6b5344"/><ellipse cx="24" cy="40" rx="16" ry="10" fill="#8a6a52"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="who">你</div>
|
||||
<div class="nums" id="you-nums"></div>
|
||||
</div>
|
||||
<button type="button" class="end" id="end">结束回合</button>
|
||||
</section>
|
||||
|
||||
<section class="row hand-row" aria-label="手牌">
|
||||
<h2>手牌</h2>
|
||||
<div class="lane hand" id="you-hand"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside class="rail">
|
||||
<h2>当前选择</h2>
|
||||
<p class="hint" id="hint">点手牌或场面开始。</p>
|
||||
|
||||
<h2>合法行动</h2>
|
||||
<div class="actions" id="actions"></div>
|
||||
|
||||
<h2>AI 本回合(Beam)</h2>
|
||||
<p class="hint">敌方用当前回合浅层搜索;每步显示分数最高的几条候选,粗体为实际选择。</p>
|
||||
<div id="ai-trace" class="ai-trace"></div>
|
||||
|
||||
<h2>最近事件</h2>
|
||||
<ol class="log" id="log"></ol>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="tooltip" id="tooltip" hidden></div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user