| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const FORMAT = "faience-game/1"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function buildRecord(session, extra = {}) { |
| if (!session) return null; |
| const state = session.state; |
| const net = extra.net || {}; |
| const info = session.opponentInfo || {}; |
|
|
| const moves = session.log |
| .filter((e) => e.kind === "move") |
| .map((e) => { |
| const move = { ply: e.ply, player: e.player, action: e.action_id }; |
| const think = session.log.find((t) => t.kind === "think" && t.ply === e.ply); |
| if (think) { |
| if (Number.isFinite(think.sims) && think.sims > 0) move.sims = think.sims; |
| |
| |
| if (Number.isFinite(think.value)) move.value = round4(think.value); |
| } |
| return move; |
| }); |
|
|
| return { |
| format: FORMAT, |
| created_at: new Date().toISOString(), |
| seed: session.seed, |
| human_seat: session.humanSeat, |
| human_first: !!session.humanPlaysFirst, |
| net: { |
| run: net.run || info.run || null, |
| checkpoint: net.checkpoint || info.checkpoint || null, |
| elo: typeof net.elo === "number" ? net.elo : typeof info.elo === "number" ? info.elo : null, |
| params: net.num_params || null, |
| backend: extra.backend || null, |
| }, |
| think_time_s: session.thinkTimeS, |
| moves, |
| deals: (session.deals || []).map((d) => ({ |
| round: d.round, |
| factories: d.factories.map((f) => f.slice()), |
| bag: d.bag.slice(), |
| lid: d.lid.slice(), |
| })), |
| final: { |
| finished: !!state.isTerminal, |
| scores: state.scores.slice(), |
| outcome: outcomeOf(session), |
| rounds: state.roundIndex + 1, |
| exhausted: !!state.exhausted, |
| }, |
| }; |
| } |
|
|
| function outcomeOf(session) { |
| const state = session.state; |
| if (!state.isTerminal) return null; |
| const mine = state.scores[session.humanSeat]; |
| const theirs = state.scores[session.aiSeat]; |
| if (mine > theirs) return "human"; |
| if (theirs > mine) return "ai"; |
| return "draw"; |
| } |
|
|
| function round4(v) { |
| return Math.round(v * 1e4) / 1e4; |
| } |
|
|
| |
| export function recordFilename(record) { |
| const bits = ["faience"]; |
| if (record.net.run) bits.push(record.net.run); |
| if (record.net.checkpoint) bits.push(record.net.checkpoint); |
| bits.push("seed" + record.seed); |
| if (record.final.outcome) bits.push(record.final.outcome); |
| return bits.join("-").replace(/[^A-Za-z0-9._-]/g, "") + ".json"; |
| } |
|
|
| |
| export const asFile = (record) => JSON.stringify(record, null, 1); |
|
|
| |
| export const asLine = (record) => JSON.stringify(record); |
|
|
| |
| |
| |
| |
| export function downloadRecord(record) { |
| try { |
| const blob = new Blob([asFile(record)], { type: "application/json" }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement("a"); |
| a.href = url; |
| a.download = recordFilename(record); |
| a.style.display = "none"; |
| document.body.appendChild(a); |
| a.click(); |
| a.remove(); |
| |
| setTimeout(() => URL.revokeObjectURL(url), 4000); |
| return true; |
| } catch (err) { |
| return false; |
| } |
| } |
|
|
| |
| export async function copyRecord(record) { |
| const text = asLine(record); |
| try { |
| await navigator.clipboard.writeText(text); |
| return true; |
| } catch (err) { |
| |
| |
| try { |
| const box = document.createElement("textarea"); |
| box.value = text; |
| box.setAttribute("readonly", ""); |
| box.style.position = "fixed"; |
| box.style.top = "-1000px"; |
| document.body.appendChild(box); |
| box.select(); |
| const ok = document.execCommand("copy"); |
| box.remove(); |
| return ok; |
| } catch (err2) { |
| return false; |
| } |
| } |
| } |
|
|