ui: drop fork tracking for new games; fix stale fork badge

New games have no reference line to return to, so rewinding to fix a finger-slip
no longer shows a 'forked' badge — corrections just overwrite silently. Loaded
games (built-in / uploaded / saved) keep 'Reset to original', but the badge now
reads 'Modified from original' (no move number), which fixes the stale-fork-point
bug: the indicator stayed pinned to the first divergence and ignored later
rewinds. Resettable correctly across any number of rewinds now.
This commit is contained in:
Michael Pilosov 2026-06-25 20:37:47 +00:00
parent 1d9f9459df
commit 85640e6f8d
3 changed files with 21 additions and 28 deletions

View File

@ -10,8 +10,8 @@
let playTimer = null; let playTimer = null;
let pressureMode = "equal"; // "equal" or "weighted" let pressureMode = "equal"; // "equal" or "weighted"
let board = null; let board = null;
let forkPoint = null; // index where fork happened, null if on original line let hasReference = false; // true for loaded games (built-in/upload/saved) that can be reset
let originalData = null; // saved original game before fork let originalData = null; // pristine copy of a reference game, captured on first edit
let showPieces = true; let showPieces = true;
let showBoard = false; let showBoard = false;
let selectedSquare = null; // tap-to-move: currently selected source square let selectedSquare = null; // tap-to-move: currently selected source square
@ -144,10 +144,11 @@
} catch (e) { } catch (e) {
return; // illegal move — ignore return; // illegal move — ignore
} }
// First divergence from the originally-loaded line: snapshot it so "Reset to original" works. // For reference games (built-in/upload/saved), snapshot the pristine line the
if (forkPoint === null && currentIndex < gameData.frames.length - 1) { // first time the user diverges from it, so "Reset to original" works. New games
// have no reference — corrections just overwrite, with no fork UI.
if (hasReference && originalData === null && currentIndex < gameData.frames.length - 1) {
originalData = JSON.parse(JSON.stringify(gameData)); originalData = JSON.parse(JSON.stringify(gameData));
forkPoint = currentIndex;
} }
// Always overwrite any continuation past the current point, so rewinding and // Always overwrite any continuation past the current point, so rewinding and
// playing a different move replaces the old line instead of appending to it. // playing a different move replaces the old line instead of appending to it.
@ -173,13 +174,11 @@
const b = gameData.moves[i + 1]; const b = gameData.moves[i + 1];
const wClass = currentIndex === i + 1 ? "active" : ""; const wClass = currentIndex === i + 1 ? "active" : "";
const bClass = b && currentIndex === i + 2 ? "active" : ""; const bClass = b && currentIndex === i + 2 ? "active" : "";
const wForked = forkPoint !== null && i >= forkPoint ? " forked" : "";
const bForked = forkPoint !== null && i + 1 >= forkPoint ? " forked" : "";
html += `<div class="move-row">`; html += `<div class="move-row">`;
html += `<span class="move-num">${num}.</span>`; html += `<span class="move-num">${num}.</span>`;
html += `<span class="move${wClass ? " " + wClass : ""}${wForked}" data-idx="${i + 1}">${w.san}</span>`; html += `<span class="move${wClass ? " " + wClass : ""}" data-idx="${i + 1}">${w.san}</span>`;
if (b) { if (b) {
html += `<span class="move${bClass ? " " + bClass : ""}${bForked}" data-idx="${i + 2}">${b.san}</span>`; html += `<span class="move${bClass ? " " + bClass : ""}" data-idx="${i + 2}">${b.san}</span>`;
} }
html += `</div>`; html += `</div>`;
} }
@ -294,20 +293,13 @@
} }
function updateForkUI() { function updateForkUI() {
const el = document.getElementById("fork-controls"); document.getElementById("fork-controls").style.display = originalData ? "flex" : "none";
if (forkPoint !== null) {
el.style.display = "flex";
document.getElementById("fork-point").textContent = Math.floor(forkPoint / 2) + 1;
} else {
el.style.display = "none";
}
} }
function resetFork() { function resetFork() {
if (!originalData) return; if (!originalData) return;
gameData = originalData; gameData = originalData;
originalData = null; originalData = null; // back on the pristine reference; still resettable after future edits
forkPoint = null;
goTo(0); goTo(0);
updateForkUI(); updateForkUI();
} }
@ -329,9 +321,9 @@
localStorage.setItem(SAVED_KEY, JSON.stringify(list)); localStorage.setItem(SAVED_KEY, JSON.stringify(list));
} }
function applyParsed(parsed, savedId) { function applyParsed(parsed, savedId, isReference) {
gameData = parsed; gameData = parsed;
forkPoint = null; hasReference = !!isReference;
originalData = null; originalData = null;
currentSavedId = savedId || null; currentSavedId = savedId || null;
currentIndex = 0; currentIndex = 0;
@ -345,7 +337,7 @@
function loadGame(gameId) { function loadGame(gameId) {
const g = BUILTIN_GAMES.find((x) => x.id === gameId); const g = BUILTIN_GAMES.find((x) => x.id === gameId);
if (!g) return; if (!g) return;
applyParsed(ChessPressure.parsePgn(g.pgn), null); applyParsed(ChessPressure.parsePgn(g.pgn), null, true);
} }
function loadSaved(savedId) { function loadSaved(savedId) {
@ -358,10 +350,12 @@
alert("That saved game could not be loaded (corrupted PGN)."); alert("That saved game could not be loaded (corrupted PGN).");
return; return;
} }
applyParsed(parsed, savedId); applyParsed(parsed, savedId, true);
} }
function loadPGN(pgn) { // isReference defaults true (uploaded games are resettable); new games pass false.
function loadPGN(pgn, isReference) {
if (isReference === undefined) isReference = true;
let parsed; let parsed;
try { try {
parsed = ChessPressure.parsePgn(pgn); parsed = ChessPressure.parsePgn(pgn);
@ -369,7 +363,7 @@
alert("Could not parse PGN — please check the format and try again."); alert("Could not parse PGN — please check the format and try again.");
return; return;
} }
applyParsed(parsed, null); applyParsed(parsed, null, isReference);
document.getElementById("game-select").value = ""; document.getElementById("game-select").value = "";
} }
@ -494,7 +488,7 @@
populateGameSelect(); populateGameSelect();
// Start on a fresh game so the player can begin immediately. // Start on a fresh game so the player can begin immediately.
loadPGN(NEW_GAME_PGN); loadPGN(NEW_GAME_PGN, false);
select.value = "__new"; select.value = "__new";
// Show board after first render (prevents tan flash) // Show board after first render (prevents tan flash)
@ -504,7 +498,7 @@
select.addEventListener("change", (e) => { select.addEventListener("change", (e) => {
const v = e.target.value; const v = e.target.value;
if (v === "__new") { if (v === "__new") {
loadPGN(NEW_GAME_PGN); loadPGN(NEW_GAME_PGN, false);
} else if (v.startsWith("saved:")) { } else if (v.startsWith("saved:")) {
loadSaved(v.slice(6)); loadSaved(v.slice(6));
} else if (v) { } else if (v) {

View File

@ -71,7 +71,7 @@
<div class="move-list" id="move-list"></div> <div class="move-list" id="move-list"></div>
<div class="fork-controls" id="fork-controls" style="display:none"> <div class="fork-controls" id="fork-controls" style="display:none">
<span class="fork-badge">Forked at move <span id="fork-point"></span></span> <span class="fork-badge">Modified from original</span>
<button id="btn-reset-fork">Reset to original</button> <button id="btn-reset-fork">Reset to original</button>
</div> </div>

View File

@ -278,7 +278,6 @@ main {
} }
.move:hover { background: var(--move-bg); } .move:hover { background: var(--move-bg); }
.move.active { background: var(--move-active); font-weight: 600; } .move.active { background: var(--move-active); font-weight: 600; }
.move.forked { border-left: 2px solid var(--fork-color); }
/* --- Fork controls --- */ /* --- Fork controls --- */
.fork-controls { .fork-controls {