Arrows for planning

This commit is contained in:
2026-02-10 17:56:21 +01:00
parent 35f8999958
commit 0802007a61

View File

@@ -15,6 +15,8 @@
--black: #1d3557; --black: #1d3557;
--white: #ffffff; --white: #ffffff;
--green: #10b981; --green: #10b981;
--arrow-primary: #f59e0b;
--arrow-secondary: #10b981;
} }
body { body {
@@ -132,6 +134,8 @@
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
user-select: none;
-webkit-user-select: none;
} }
.edge { .edge {
@@ -175,7 +179,29 @@
stroke-dasharray: 4; stroke-dasharray: 4;
} }
/* Victory Banner */ .arrow {
pointer-events: none;
stroke-width: 4;
stroke-linecap: round;
opacity: 1;
}
.arrow-pri {
stroke: var(--arrow-primary);
}
.arrow-sec {
stroke: var(--arrow-secondary);
}
#head-pri {
fill: var(--arrow-primary);
}
#head-sec {
fill: var(--arrow-secondary);
}
#victory-banner { #victory-banner {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -329,7 +355,7 @@
</style> </style>
</head> </head>
<body> <body oncontextmenu="return false;">
<div id="lobby" class="main-card"> <div id="lobby" class="main-card">
<h1 style="text-align: center; margin: 0; border-bottom: 4px solid var(--black); padding-bottom: 10px;">WOLF & <h1 style="text-align: center; margin: 0; border-bottom: 4px solid var(--black); padding-bottom: 10px;">WOLF &
@@ -364,7 +390,16 @@
<p style="margin: 5px 0 0 0; font-size: 0.8rem;">SPIEL BEENDET</p> <p style="margin: 5px 0 0 0; font-size: 0.8rem;">SPIEL BEENDET</p>
</div> </div>
<svg viewBox="0 0 600 420"></svg> <svg viewBox="0 0 600 420">
<defs>
<marker id="arrowhead-pri" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
<polygon points="0 0, 6 2, 0 4" id="head-pri" />
</marker>
<marker id="arrowhead-sec" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto">
<polygon points="0 0, 6 2, 0 4" id="head-sec" />
</marker>
</defs>
</svg>
</div> </div>
<div class="controls"> <div class="controls">
@@ -384,11 +419,15 @@
let roomId = params.get("room"); let roomId = params.get("room");
let isLocal = params.get("mode") === "local"; let isLocal = params.get("mode") === "local";
// --- STATE & HISTORY ---
let ws, myRole, selected = null; let ws, myRole, selected = null;
let state = null; let state = null;
let history = []; let history = [];
let historyIndex = -1; let historyIndex = -1;
let plannedArrows = [];
let dragStartNode = null;
let dragStartTime = 0;
let rightClickStart = null;
const pos = { 0: [80, 210], 1: [180, 110], 2: [180, 210], 3: [180, 310], 4: [280, 110], 5: [280, 210], 6: [280, 310], 7: [380, 110], 8: [380, 210], 9: [380, 310], 10: [480, 210] }; const pos = { 0: [80, 210], 1: [180, 110], 2: [180, 210], 3: [180, 310], 4: [280, 110], 5: [280, 210], 6: [280, 310], 7: [380, 110], 8: [380, 210], 9: [380, 310], 10: [480, 210] };
const edges = [[0, 1], [0, 2], [0, 3], [1, 2], [2, 3], [1, 4], [2, 5], [3, 6], [4, 5], [5, 6], [4, 7], [5, 8], [6, 9], [7, 8], [8, 9], [5, 1], [5, 3], [5, 7], [5, 9], [7, 10], [8, 10], [9, 10]]; const edges = [[0, 1], [0, 2], [0, 3], [1, 2], [2, 3], [1, 4], [2, 5], [3, 6], [4, 5], [5, 6], [4, 7], [5, 8], [6, 9], [7, 8], [8, 9], [5, 1], [5, 3], [5, 7], [5, 9], [7, 10], [8, 10], [9, 10]];
@@ -410,6 +449,7 @@
const isAtEnd = historyIndex === history.length - 1; const isAtEnd = historyIndex === history.length - 1;
if (newState.moveCount === 0) { if (newState.moveCount === 0) {
history = [JSON.parse(JSON.stringify(newState))]; history = [JSON.parse(JSON.stringify(newState))];
plannedArrows = [];
} else if (!state || newState.moveCount > state.moveCount) { } else if (!state || newState.moveCount > state.moveCount) {
history.push(JSON.parse(JSON.stringify(newState))); history.push(JSON.parse(JSON.stringify(newState)));
} }
@@ -466,35 +506,39 @@
nextState.moveCount++; nextState.moveCount++;
nextState.winner = checkWinConditions(nextState); nextState.winner = checkWinConditions(nextState);
history = history.slice(0, historyIndex + 1); history = history.slice(0, historyIndex + 1);
plannedArrows = [];
updateHistory(nextState, true); updateHistory(nextState, true);
} }
function checkWinConditions(g) { function checkWinConditions(g) {
// 1. Wolf reaches sheep starting line (Row 0)
if (g.wolf === 0) return "wolf"; if (g.wolf === 0) return "wolf";
// 2. Max moves reached
if (g.moveCount >= 40) return "wolf"; if (g.moveCount >= 40) return "wolf";
// 3. Wolf is trapped
const canWolfMove = bWolf[g.wolf].some(to => !g.sheep.includes(to)); const canWolfMove = bWolf[g.wolf].some(to => !g.sheep.includes(to));
if (!canWolfMove) return "sheep"; if (!canWolfMove) return "sheep";
// 4. Sheep are trapped (Wolf wins)
const canAnySheepMove = g.sheep.some(sPos => { const canAnySheepMove = g.sheep.some(sPos => {
return bSheep[sPos].some(to => to !== g.wolf && !g.sheep.includes(to)); return bSheep[sPos].some(to => to !== g.wolf && !g.sheep.includes(to));
}); });
if (!canAnySheepMove) return "wolf"; if (!canAnySheepMove) return "wolf";
return null; return null;
} }
function toggleArrow(from, to, type) {
if (from === to || !bWolf[from].includes(to)) return;
const idx = plannedArrows.findIndex(a => a.from === from && a.to === to && a.type === type);
if (idx > -1) plannedArrows.splice(idx, 1);
else plannedArrows.push({ from, to, type });
render();
}
function render() { function render() {
const viewState = history[historyIndex]; const viewState = history[historyIndex];
if (!viewState) return; if (!viewState) return;
const isViewingHistory = historyIndex < history.length - 1; const isViewingHistory = historyIndex < history.length - 1;
const svg = document.querySelector("svg"); const svg = document.querySelector("svg");
const defs = svg.querySelector("defs");
svg.innerHTML = ""; svg.innerHTML = "";
svg.appendChild(defs);
edges.forEach(([a, b]) => { edges.forEach(([a, b]) => {
const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
@@ -537,17 +581,42 @@
const isPossible = canInteract && selected !== null && (viewState.turn === "wolf" ? bWolf[selected] : bSheep[selected]).includes(i) && !isWolf && !isSheep; const isPossible = canInteract && selected !== null && (viewState.turn === "wolf" ? bWolf[selected] : bSheep[selected]).includes(i) && !isWolf && !isSheep;
const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.onclick = () => {
if (viewState.winner || (!isLocal && isViewingHistory)) return; g.onmousedown = (e) => {
if (!isLocal && state.turn !== myRole) return; if (e.button === 0) {
const isMine = (viewState.turn === "wolf" && isWolf) || (viewState.turn === "sheep" && isSheep); dragStartNode = i;
if (isMine) selected = i; dragStartTime = Date.now();
else if (isPossible) { }
if (isLocal) handleLocalMove(selected, i); if (e.button === 2) rightClickStart = i;
else ws.send(JSON.stringify({ from: selected, to: i })); };
selected = null;
} else selected = null; g.onmouseup = (e) => {
render(); if (e.button === 0 && dragStartNode !== null) {
const duration = Date.now() - dragStartTime;
if (dragStartNode === i && duration < 250) {
if (viewState.winner || (!isLocal && isViewingHistory)) return;
if (!isLocal && state.turn !== myRole) return;
const isMine = (viewState.turn === "wolf" && isWolf) || (viewState.turn === "sheep" && isSheep);
if (isMine) selected = i;
else if (isPossible) {
plannedArrows = [];
if (isLocal) handleLocalMove(selected, i);
else ws.send(JSON.stringify({ from: selected, to: i }));
selected = null;
} else selected = null;
render();
} else if (dragStartNode !== i) {
toggleArrow(dragStartNode, i, 'sec');
}
dragStartNode = null;
}
if (e.button === 2 && rightClickStart !== null) {
if (rightClickStart !== i) toggleArrow(rightClickStart, i, 'pri');
else plannedArrows = [];
rightClickStart = null;
render();
}
}; };
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle"); const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
@@ -563,6 +632,20 @@
t.textContent = i; g.appendChild(t); t.textContent = i; g.appendChild(t);
svg.appendChild(g); svg.appendChild(g);
} }
plannedArrows.forEach(arrow => {
const s = pos[arrow.from]; const e = pos[arrow.to];
const dx = e[0] - s[0]; const dy = e[1] - s[1];
const angle = Math.atan2(dy, dx);
const x1 = s[0] + Math.cos(angle) * 28; const y1 = s[1] + Math.sin(angle) * 28;
const x2 = e[0] - Math.cos(angle) * 32; const y2 = e[1] - Math.sin(angle) * 32;
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", x1); line.setAttribute("y1", y1);
line.setAttribute("x2", x2); line.setAttribute("y2", y2);
line.setAttribute("class", `arrow ${arrow.type === 'pri' ? 'arrow-pri' : 'arrow-sec'}`);
line.setAttribute("marker-end", `url(#arrowhead-${arrow.type})`);
svg.appendChild(line);
});
} }
function updateRoomList(rooms) { function updateRoomList(rooms) {
@@ -582,6 +665,7 @@
function startLocal() { window.location.search = `?mode=local`; } function startLocal() { window.location.search = `?mode=local`; }
document.getElementById("restartBtn").onclick = () => { document.getElementById("restartBtn").onclick = () => {
plannedArrows = [];
if (isLocal) initLocalState(); if (isLocal) initLocalState();
else ws.send(JSON.stringify({ type: 'restart' })); else ws.send(JSON.stringify({ type: 'restart' }));
}; };