Arrows for planning
This commit is contained in:
@@ -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 = () => {
|
|
||||||
|
g.onmousedown = (e) => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
dragStartNode = i;
|
||||||
|
dragStartTime = Date.now();
|
||||||
|
}
|
||||||
|
if (e.button === 2) rightClickStart = i;
|
||||||
|
};
|
||||||
|
|
||||||
|
g.onmouseup = (e) => {
|
||||||
|
if (e.button === 0 && dragStartNode !== null) {
|
||||||
|
const duration = Date.now() - dragStartTime;
|
||||||
|
if (dragStartNode === i && duration < 250) {
|
||||||
if (viewState.winner || (!isLocal && isViewingHistory)) return;
|
if (viewState.winner || (!isLocal && isViewingHistory)) return;
|
||||||
if (!isLocal && state.turn !== myRole) return;
|
if (!isLocal && state.turn !== myRole) return;
|
||||||
const isMine = (viewState.turn === "wolf" && isWolf) || (viewState.turn === "sheep" && isSheep);
|
const isMine = (viewState.turn === "wolf" && isWolf) || (viewState.turn === "sheep" && isSheep);
|
||||||
if (isMine) selected = i;
|
if (isMine) selected = i;
|
||||||
else if (isPossible) {
|
else if (isPossible) {
|
||||||
|
plannedArrows = [];
|
||||||
if (isLocal) handleLocalMove(selected, i);
|
if (isLocal) handleLocalMove(selected, i);
|
||||||
else ws.send(JSON.stringify({ from: selected, to: i }));
|
else ws.send(JSON.stringify({ from: selected, to: i }));
|
||||||
selected = null;
|
selected = null;
|
||||||
} else selected = null;
|
} else selected = null;
|
||||||
render();
|
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' }));
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user