Simplify pipe puzzle to pure spanning-tree boards
- Remove extra-edge generation and cross pieces; board is now a single random spanning tree (one unique route between every pair of cells, no loops) - Update leak rendering to place droplets on exact open edges using DELTA/EDGE and render water/flash layers above tiles in the container - Adjust mini preview tiles and difficulty config (drop `extra` param) - Rework tutorial to explain tree structure, scattered dead ends, and branch-by-branch solving strategy - Update verification tool: assert tree edge count, no 4-way cells, leaf distribution across rows, faucet row variety, and board randomness; replace cross-based fixture with a valid tree
This commit is contained in:
parent
d27244ac60
commit
a4efa5b68a
|
|
@ -13,7 +13,7 @@ import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
|||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { ensureTileTextures, angleFor, tileKeyFor, TILE_PX } from './PipePuzzleArt.js';
|
||||
import {
|
||||
N, E, S, W, DIRS, OPP,
|
||||
N, E, S, W, DIRS, OPP, DELTA,
|
||||
cellRC, matchedDirs, neighborOf,
|
||||
generatePuzzle, rotateAt, isSolved, wetOrder, countLeaks,
|
||||
DIFFICULTIES, difficultyByKey,
|
||||
|
|
@ -150,8 +150,8 @@ export default class PipePuzzleGame extends Phaser.Scene {
|
|||
const MINI = [
|
||||
{ tex: 'pp-tile-elbow', angle: 0 },
|
||||
{ tex: 'pp-tile-t', angle: 180 },
|
||||
{ tex: 'pp-tile-cross', angle: 0 },
|
||||
{ tex: 'pp-tile-stub', angle: 0 },
|
||||
{ tex: 'pp-tile-stub', angle: 90 },
|
||||
{ tex: 'pp-tile-straight', angle: 0 },
|
||||
];
|
||||
|
||||
DIFFICULTIES.forEach((diff, i) => {
|
||||
|
|
@ -240,7 +240,7 @@ export default class PipePuzzleGame extends Phaser.Scene {
|
|||
this._screen = 'play';
|
||||
this._diff = diffKey;
|
||||
const diff = difficultyByKey(diffKey);
|
||||
this._board = generatePuzzle(diff.n, diff.extra ?? 0);
|
||||
this._board = generatePuzzle(diff.n);
|
||||
this._moves = 0;
|
||||
this._time0 = null;
|
||||
this._won = false;
|
||||
|
|
@ -274,10 +274,13 @@ export default class PipePuzzleGame extends Phaser.Scene {
|
|||
}
|
||||
sc.add(frame);
|
||||
|
||||
// Water layer (under tiles? no — over tiles, low alpha so pipes show through).
|
||||
this._waterGfx = this.add.graphics().setDepth(D.water);
|
||||
this._flashGfx = this.add.graphics().setDepth(D.flash);
|
||||
sc.add([this._waterGfx, this._flashGfx]);
|
||||
// Water & flash layers. Phaser Containers render children in list order
|
||||
// (children's depths are ignored inside a Container), so these MUST be
|
||||
// added AFTER the tiles to draw the water flow + leak dots in FRONT of
|
||||
// the pipes — semi-transparent, so the pipes still show through.
|
||||
this._waterGfx = this.add.graphics();
|
||||
this._flashGfx = this.add.graphics();
|
||||
// (added to the container below, after the tiles)
|
||||
|
||||
// ── Tiles ──
|
||||
this._cells = [];
|
||||
|
|
@ -304,6 +307,9 @@ export default class PipePuzzleGame extends Phaser.Scene {
|
|||
sc.add(img);
|
||||
}
|
||||
|
||||
// Water + flash layers go last → they render above the tiles.
|
||||
sc.add([this._waterGfx, this._flashGfx]);
|
||||
|
||||
// ── Header ──
|
||||
const back = new Button(this, 100, 52, '← Menu',
|
||||
() => { playSound(this, SFX.UI_PICK); this._showSelect(); },
|
||||
|
|
@ -456,21 +462,28 @@ export default class PipePuzzleGame extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
// 3) Leak droplets (wet cells with an open end) — pulsing red.
|
||||
// 3) Leak droplets — one per leaking socket on the WHOLE board, placed on
|
||||
// the exact edge of the leaking tile where the pipe's open end is. The
|
||||
// leak test is identical to countLeaks, so the number of dots always
|
||||
// equals the LEAKS counter.
|
||||
const pulse = 0.55 + 0.35 * Math.sin(this.time.now * 0.008);
|
||||
for (const i of order) {
|
||||
for (let i = 0; i < n * n; i++) {
|
||||
if (sockets[i] === 0) continue;
|
||||
const [r, c] = cellRC(i, n);
|
||||
for (const d of DIRS) {
|
||||
if (!(sockets[i] & d)) continue;
|
||||
const [dr, dc] = EDGE[d];
|
||||
const nr = r + dr, nc = c + dc;
|
||||
if (nr >= 0 && nr < n && nc >= 0 && nc < n && (sockets[nr * n + nc] & OPP[d])) continue;
|
||||
const x = cx(i) + dc * CELL * 0.5;
|
||||
const y = cy(i) + dr * CELL * 0.5;
|
||||
const [gr, gc] = DELTA[d]; // grid [row, col] delta — for the neighbor test
|
||||
const nr = r + gr, nc = c + gc;
|
||||
if (nr >= 0 && nr < n && nc >= 0 && nc < n && (sockets[nr * n + nc] & OPP[d])) continue; // matched — no leak
|
||||
const [ex, ey] = EDGE[d]; // pixel [x, y] delta — for the dot position
|
||||
// Sit the dot just inside the tile's open edge (at the pipe mouth),
|
||||
// so it reads as water escaping from that exact opening.
|
||||
const x = cx(i) + ex * CELL * 0.42;
|
||||
const y = cy(i) + ey * CELL * 0.42;
|
||||
gfx.fillStyle(0xff5a5a, pulse * 0.9);
|
||||
gfx.fillCircle(x, y, CELL * 0.075);
|
||||
gfx.fillCircle(x, y, CELL * 0.09);
|
||||
gfx.fillStyle(0x7a1d24, pulse);
|
||||
gfx.fillCircle(x, y, CELL * 0.035);
|
||||
gfx.fillCircle(x, y, CELL * 0.045);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,24 @@
|
|||
// • N×N grid, EVERY cell holds a pipe tile (no empty squares).
|
||||
// • A board is a set of EDGES between adjacent cells. A cell's degree =
|
||||
// how many sockets it has:
|
||||
// 1 → stub (dead end) 2 → straight or elbow
|
||||
// 3 → T-piece 4 → cross
|
||||
// 1 → stub (dead end) 2 → straight or elbow 3 → T-piece
|
||||
// plus two fixed anchors the player cannot rotate:
|
||||
// source — the faucet (degree 1)
|
||||
// drain — the drain (degree 1)
|
||||
// • Generation: a random SPANNING TREE (touches every cell, so no empty
|
||||
// squares and the board is connected) plus a tunable number of EXTRA
|
||||
// edges, which create cycles and raise cell degrees so the board is full
|
||||
// of T-pieces, crosses and branching — a maze, not a single line.
|
||||
// • Generation: a random SPANNING TREE over the grid. A tree has exactly
|
||||
// one path between any two cells, so from the faucet to every dead end
|
||||
// there is a single route — no loops, no shortcuts, no crosses. Because
|
||||
// the tree touches every cell, the board is always fully filled and
|
||||
// connected, and the degree-1 leaves (dead ends) end up scattered
|
||||
// anywhere on the grid.
|
||||
// • Always solvable: the solved pose (each cell's sockets pointing at its
|
||||
// neighbours in the edge set) is leak-free by construction, and every tile
|
||||
// shape (stub/straight/elbow/T/cross) can be rotated to any orientation of
|
||||
// that shape, so that pose is always reachable by the player.
|
||||
// tree-neighbours) is leak-free by construction, and every tile shape
|
||||
// (stub/straight/elbow/T) can be rotated to any orientation of that
|
||||
// shape, so that pose is always reachable by the player.
|
||||
// • Scramble = rotate every non-anchor tile by a random multiple of 90°.
|
||||
// • WIN = no leaks anywhere: every socket is matched to a neighbour that
|
||||
// opens back. There can be many valid solutions — you just need to find one.
|
||||
// opens back. There can be many valid solutions — you just need to find
|
||||
// one (different tiles can point different ways and still be leak-free).
|
||||
|
||||
// ── Directions ───────────────────────────────────────────────────────────────
|
||||
export const N = 1, E = 2, S = 4, W = 8;
|
||||
|
|
@ -104,8 +106,7 @@ function allEdges(n) {
|
|||
// of cell i in the solved pose (a bit set for each tree-neighbour direction).
|
||||
function randomSpanningTree(n) {
|
||||
const N2 = n * n;
|
||||
const edges = allEdges(n);
|
||||
shuffle(edges);
|
||||
const edges = shuffle(allEdges(n)); // shuffle() returns a copy — use it!
|
||||
const parent = new Array(N2);
|
||||
for (let i = 0; i < N2; i++) parent[i] = i;
|
||||
const find = (x) => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; };
|
||||
|
|
@ -123,16 +124,18 @@ function randomSpanningTree(n) {
|
|||
return tree;
|
||||
}
|
||||
|
||||
// A good base: at least a couple of branch points (T/cross) and a few dead
|
||||
// ends so the board reads as a maze.
|
||||
// A good base: a couple of branch points (T-pieces) and several dead ends so
|
||||
// the board reads as a maze — and no 4-way cell, so no cross pieces ever
|
||||
// appear (a crossing would be the only place two routes could merge).
|
||||
function goodBase(n, tree) {
|
||||
let leaves = 0, branch = 0;
|
||||
let leaves = 0, branch = 0, cross = 0;
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const d = bitCount(tree[i]);
|
||||
if (d === 1) leaves++;
|
||||
else if (d >= 3) branch++;
|
||||
else if (d === 3) branch++;
|
||||
else if (d === 4) cross++;
|
||||
}
|
||||
return branch >= 2 && leaves >= 3;
|
||||
return branch >= 2 && leaves >= 3 && cross === 0;
|
||||
}
|
||||
|
||||
// Pick two far-apart degree-1 cells to be the faucet & drain.
|
||||
|
|
@ -154,46 +157,14 @@ function pickAnchorPair(n, sockets) {
|
|||
return Math.random() < 0.5 ? [bestA, bestB] : [bestB, bestA];
|
||||
}
|
||||
|
||||
// Does the final board have a good mix of interesting pieces?
|
||||
function hasGoodMix(n, sockets) {
|
||||
let tc = 0, stubs = 0;
|
||||
for (let i = 0; i < sockets.length; i++) {
|
||||
const d = bitCount(sockets[i]);
|
||||
if (d >= 3) tc++;
|
||||
else if (d === 1) stubs++;
|
||||
}
|
||||
return tc >= 3 && stubs >= 3;
|
||||
}
|
||||
|
||||
export function generatePuzzle(n, extra = 0) {
|
||||
export function generatePuzzle(n) {
|
||||
// A random spanning tree: touches every cell (fully filled board), is
|
||||
// connected, and has exactly one path between any two cells — so every
|
||||
// dead end is reached by a single route and the faucet can be anywhere.
|
||||
let base = null, tries = 0;
|
||||
do { base = randomSpanningTree(n); tries++; } while (!goodBase(n, base) && tries < 300);
|
||||
|
||||
// Add `extra` random edges (not already in the tree) to create cycles and
|
||||
// raise degrees → more T-pieces and crosses.
|
||||
const treeEdges = new Set();
|
||||
for (let i = 0; i < n * n; i++) {
|
||||
for (const d of [E, S]) { // each edge once
|
||||
if (!(base[i] & d)) continue;
|
||||
const [dr, dc] = { [E]: [0, 1], [S]: [1, 0] }[d];
|
||||
const j = (Math.floor(i / n) + dr) * n + (i % n + dc);
|
||||
treeEdges.add(Math.min(i, j) * 10000 + Math.max(i, j));
|
||||
}
|
||||
}
|
||||
const candidates = allEdges(n).filter(([a, b]) => !treeEdges.has(Math.min(a, b) * 10000 + Math.max(a, b)));
|
||||
|
||||
let sockets, mixTries = 0;
|
||||
do {
|
||||
sockets = base.slice();
|
||||
const picked = shuffle(candidates).slice(0, extra);
|
||||
for (const [a, b] of picked) {
|
||||
const d = dirBetween(a, b, n);
|
||||
sockets[a] |= d;
|
||||
sockets[b] |= OPP[d];
|
||||
}
|
||||
mixTries++;
|
||||
} while (!hasGoodMix(n, sockets) && mixTries < 300);
|
||||
|
||||
const sockets = base;
|
||||
const [source, drain] = pickAnchorPair(n, sockets);
|
||||
|
||||
// Scramble non-anchor tiles (anchors stay fixed).
|
||||
|
|
@ -271,13 +242,13 @@ export function rotateAt(board, i) {
|
|||
}
|
||||
|
||||
// ── Difficulty tiers ─────────────────────────────────────────────────────────
|
||||
// `extra` = number of extra edges added on top of the spanning tree. Higher
|
||||
// → more T-pieces and crosses, denser and harder to trace.
|
||||
// Grid size is the difficulty knob: bigger tree → longer single routes, more
|
||||
// branch points (T-pieces) and dead ends to keep straight.
|
||||
export const DIFFICULTIES = [
|
||||
{ key: 'easy', label: 'Easy', n: 6, extra: 5, blurb: '6 × 6 grid' },
|
||||
{ key: 'medium', label: 'Medium', n: 7, extra: 9, blurb: '7 × 7 grid' },
|
||||
{ key: 'hard', label: 'Hard', n: 8, extra: 15, blurb: '8 × 8 grid' },
|
||||
{ key: 'expert', label: 'Expert', n: 10, extra: 26, blurb: '10 × 10 grid' },
|
||||
{ key: 'easy', label: 'Easy', n: 6, blurb: '6 × 6 grid' },
|
||||
{ key: 'medium', label: 'Medium', n: 7, blurb: '7 × 7 grid' },
|
||||
{ key: 'hard', label: 'Hard', n: 8, blurb: '8 × 8 grid' },
|
||||
{ key: 'expert', label: 'Expert', n: 10, blurb: '10 × 10 grid' },
|
||||
];
|
||||
export function difficultyByKey(key) {
|
||||
return DIFFICULTIES.find((d) => d.key === key) ?? DIFFICULTIES[0];
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ when not a single socket is left open. No leaks allowed.
|
|||
- An open end that points at a wall, or at a tile whose socket isn't open, is
|
||||
a **leak** (it glows red). Leaks keep the puzzle unsolved.
|
||||
|
||||
## How the Board Works
|
||||
|
||||
- The network is a **branching tree**: from the faucet there is exactly
|
||||
**one route** to every dead end. No loops, no shortcuts.
|
||||
- Dead ends can be **anywhere** on the grid — top, bottom, left, right. Don't
|
||||
assume the "exit" is on one side.
|
||||
- The faucet (brass) and the drain (iron) are the two long routes; the rest
|
||||
of the dead ends hang off branch points.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Click (or tap) a pipe** to rotate it 90° clockwise.
|
||||
|
|
@ -28,9 +37,8 @@ when not a single socket is left open. No leaks allowed.
|
|||
- **Elbow pipes** (corner collar with three bolts) turn water 90°.
|
||||
- **T-pieces** (a tee body with three arms) split the flow — one arm is the
|
||||
"dead end" of that branch.
|
||||
- **Crosses** (the big four-arm fitting) meet four pipes.
|
||||
- **Dead ends** (a single capped stub) are the red herrings — they look like
|
||||
they could be the path but aren't.
|
||||
- **Dead ends** (a single capped stub) are the leaves of the tree — each one
|
||||
sits at the end of exactly one route from the faucet.
|
||||
- **Water** (the blue glow) shows everything currently connected to the
|
||||
faucet; the animated pulses show the flow direction.
|
||||
- The **LEAKS** counter tells you how many open ends remain. Solved = 0 leaks.
|
||||
|
|
@ -59,9 +67,9 @@ when not a single socket is left open. No leaks allowed.
|
|||
That arm must end in a dead-end stub. Find the matching stub and you've
|
||||
locked in the T's orientation.
|
||||
- **Dead ends are your clues.** A capped stub must rotate to face the correct
|
||||
neighbour. If two stubs must both face the same cell, that cell has to be a
|
||||
T or cross — rotate the surrounding pieces to make room.
|
||||
- **Work in regions.** Solve a corner or a branch, then lock it in and move
|
||||
to the next. Don't chase a single long path.
|
||||
neighbour. Since there's exactly one route to each dead end, finding the
|
||||
stub locks in every tile along that whole branch.
|
||||
- **Work branch by branch.** Solve one dead end's route, lock it in, then take
|
||||
the next branch off the nearest T-piece.
|
||||
- **If you're stuck**, check for pairs of stubs that must both face the same
|
||||
tile — that's usually the key move.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
rotateSockets, bitCount, generatePuzzle, isSolved, countLeaks,
|
||||
wetOrder, DIFFICULTIES,
|
||||
} from '../src/games/pipepuzzle/PipePuzzleLogic.js';
|
||||
|
||||
let failures = 0;
|
||||
function check(name, cond, detail = '') {
|
||||
if (cond) { console.log(` ok ${name}`); }
|
||||
|
|
@ -29,21 +28,23 @@ check('rotateSockets elbow NE→ES→SW→WN', rotateSockets(N | E, 1) === (E |
|
|||
check('rotateSockets T N|E|W → N|S|E', rotateSockets(N | E | W, 1) === (N | S | E));
|
||||
check('rotateSockets cross = invariant', rotateSockets(N | E | S | W, 3) === (N | E | S | W));
|
||||
|
||||
// A solved 3×3 no-leak board (row-major 0..8):
|
||||
// 0=E (source) 1=W|E|S (T) 2=W (stub)
|
||||
// 3=E|S (elbow) 4=N|E|S|W (cross) 5=W (stub)
|
||||
// 6=N (stub) 7=N|E (elbow) 8=W (drain)
|
||||
// Every socket is matched to a neighbour that opens back — no leaks.
|
||||
// A solved 3×3 no-leak board (row-major 0..8) — a plain spanning tree (8
|
||||
// edges, no cycles, no 4-way cell) with two T-pieces and four dead ends:
|
||||
// 0=E (source) 1=W|E|S (T) 2=W (stub)
|
||||
// 3=E (stub) 4=N|W|E (T) 5=W|S (elbow)
|
||||
// 6=E (drain) 7=E|W (straight) 8=N|W (elbow)
|
||||
// Every socket is matched to a neighbour that opens back, so no leaks.
|
||||
{
|
||||
const n = 3;
|
||||
const sockets = [E, W | E | S, W, E | S, N | E | S | W, W, N, N | E, W];
|
||||
const board = { n, sockets, source: 0, drain: 8 };
|
||||
const sockets = [E, W | E | S, W, E, N | W | E, W | S, E, E | W, N | W];
|
||||
const board = { n, sockets, source: 0, drain: 6 };
|
||||
check('fixture 3×3 solved (no leaks)', isSolved(board) === true);
|
||||
check('fixture 3×3 has a T-piece (3 sockets)', sockets.filter((s) => bitCount(s) === 3).length >= 1);
|
||||
check('fixture 3×3 has a cross (4 sockets)', sockets.some((s) => bitCount(s) === 4));
|
||||
check('fixture 3×3 has no cross (4 sockets)', !sockets.some((s) => bitCount(s) === 4));
|
||||
|
||||
// Break one connection: rotate the stub at cell2 W → N (points off the wall).
|
||||
board.sockets = [E, W | E | S, N, E | S, N | E | S | W, W, N, N | E, W];
|
||||
// Break one connection: rotate the stub at cell2 W → N (points off the
|
||||
// top wall). Off-board socket = guaranteed leak.
|
||||
board.sockets = [E, W | E | S, N, E, N | W | E, W | S, E, E | W, N | W];
|
||||
check('fixture 3×3 after rotation not solved', isSolved(board) === false);
|
||||
check('fixture 3×3 after rotation has ≥1 leak', countLeaks(board.sockets, n) >= 1);
|
||||
}
|
||||
|
|
@ -71,28 +72,49 @@ function isConnected(n, sockets) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Number of matched edges on the board (a tree over n² cells has exactly n²−1).
|
||||
function matchedEdgeCount(n, sockets) {
|
||||
let edges = 0;
|
||||
for (let i = 0; i < n * n; i++) {
|
||||
if (sockets[i] & E) edges++; // count each E-neighbour once
|
||||
if (sockets[i] & S) edges++; // count each S-neighbour once
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
console.log('\n— Generation invariants —');
|
||||
for (const diff of DIFFICULTIES) {
|
||||
const n = diff.n;
|
||||
const samples = 30;
|
||||
let solNoLeak = 0, scrLeak = 0, mixOk = 0, connOk = 0, wetAll = 0;
|
||||
let solNoLeak = 0, scrLeak = 0, connOk = 0, wetAll = 0, treeOk = 0, noCross = 0;
|
||||
const solutions = new Set();
|
||||
const leafRowsSeen = new Array(n).fill(false);
|
||||
const srcRowsSeen = new Array(n).fill(false);
|
||||
|
||||
for (let s = 0; s < samples; s++) {
|
||||
const p = generatePuzzle(n, diff.extra ?? 0);
|
||||
const p = generatePuzzle(n);
|
||||
if (countLeaks(p.solution, n) === 0) solNoLeak++;
|
||||
if (countLeaks(p.sockets, n) > 0) scrLeak++;
|
||||
const kinds = p.solution.map((sk) => bitCount(sk));
|
||||
const tc = kinds.filter((d) => d >= 3).length;
|
||||
const stubs = kinds.filter((d) => d === 1).length;
|
||||
if (tc >= 3 && stubs >= 3) mixOk++;
|
||||
if (isConnected(n, p.solution)) connOk++;
|
||||
if (wetOrder(p.solution, n, p.source).length === n * n) wetAll++;
|
||||
// A spanning tree: exactly n²−1 matched edges → no cycles → one unique
|
||||
// route between every pair of cells (faucet → each dead end).
|
||||
if (matchedEdgeCount(n, p.solution) === n * n - 1) treeOk++;
|
||||
if (!p.solution.some((sk) => bitCount(sk) === 4)) noCross++;
|
||||
for (let i = 0; i < n * n; i++) if (bitCount(p.solution[i]) === 1) leafRowsSeen[Math.floor(i / n)] = true;
|
||||
srcRowsSeen[Math.floor(p.source / n)] = true;
|
||||
solutions.add(JSON.stringify(p.solution));
|
||||
}
|
||||
|
||||
const leafRowsAll = leafRowsSeen.every(Boolean);
|
||||
check(`${diff.key} (${n}×${n}): solution board has no leaks (${solNoLeak}/${samples})`, solNoLeak === samples);
|
||||
check(`${diff.key} (${n}×${n}): board is connected (${connOk}/${samples})`, connOk === samples);
|
||||
check(`${diff.key} (${n}×${n}): faucet reaches every cell (${wetAll}/${samples})`, wetAll === samples);
|
||||
check(`${diff.key} (${n}×${n}): rich mix — ≥3 T/cross + ≥3 dead-ends (${mixOk}/${samples})`, mixOk === samples);
|
||||
check(`${diff.key} (${n}×${n}): solution is a tree — exactly ${n * n - 1} edges, no loops (${treeOk}/${samples})`, treeOk === samples);
|
||||
check(`${diff.key} (${n}×${n}): no 4-way crosses on the board (${noCross}/${samples})`, noCross === samples);
|
||||
check(`${diff.key} (${n}×${n}): dead ends appear in every row across boards`, leafRowsAll);
|
||||
check(`${diff.key} (${n}×${n}): faucet appears in more than one row across boards`, srcRowsSeen.filter(Boolean).length >= 2);
|
||||
check(`${diff.key} (${n}×${n}): boards are genuinely random (≥15 distinct of ${samples})`, solutions.size >= 15);
|
||||
check(`${diff.key} (${n}×${n}): scrambled board starts with leaks (${scrLeak}/${samples})`, scrLeak === samples);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue