Spaces:
Running
Running
nilshoehing commited on
Commit ·
b8d1810
1
Parent(s): dc70f91
Fix bridges visible crossing cycle
Browse files
frontend/src/components/PuzzleEditor.tsx
CHANGED
|
@@ -106,6 +106,11 @@ function bridgeLevelFromChar(opportunity: BridgesOpportunity, char: string): 0 |
|
|
| 106 |
return char === '"' ? 2 : char === "|" ? 1 : 0;
|
| 107 |
}
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
function parseBridges(problemAscii: string) {
|
| 110 |
const lines = splitLines(problemAscii);
|
| 111 |
const opportunities: BridgesOpportunity[] = [];
|
|
@@ -201,8 +206,10 @@ function bridgeClickCountForOpportunity(
|
|
| 201 |
}
|
| 202 |
|
| 203 |
function cycleBridgesOpportunities(
|
|
|
|
| 204 |
states: Record<string, 0 | 1 | 2>,
|
| 205 |
opportunities: BridgesOpportunity[],
|
|
|
|
| 206 |
): Record<string, 0 | 1 | 2> {
|
| 207 |
if (opportunities.length === 0) {
|
| 208 |
return states;
|
|
@@ -215,6 +222,13 @@ function cycleBridgesOpportunities(
|
|
| 215 |
const opportunity = candidates[0];
|
| 216 |
const level = (((nextStates[opportunity.id] ?? 0) + 1) % 3) as 0 | 1 | 2;
|
| 217 |
nextStates[opportunity.id] = level;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return nextStates;
|
| 219 |
}
|
| 220 |
|
|
@@ -225,28 +239,13 @@ function cycleBridgesOpportunities(
|
|
| 225 |
]),
|
| 226 |
null,
|
| 227 |
];
|
| 228 |
-
const
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
let currentIndex = sequence.findIndex(isSequenceMatch);
|
| 234 |
-
|
| 235 |
-
if (currentIndex === -1) {
|
| 236 |
-
const active = candidates.find((opportunity) => (nextStates[opportunity.id] ?? 0) > 0);
|
| 237 |
-
currentIndex = active
|
| 238 |
-
? sequence.findIndex(
|
| 239 |
-
(entry) =>
|
| 240 |
-
entry?.opportunity.id === active.id &&
|
| 241 |
-
entry.level === (nextStates[active.id] ?? 0),
|
| 242 |
-
)
|
| 243 |
-
: sequence.length - 1;
|
| 244 |
-
if (currentIndex === -1) {
|
| 245 |
-
currentIndex = sequence.length - 1;
|
| 246 |
-
}
|
| 247 |
-
}
|
| 248 |
|
| 249 |
-
const nextEntry = sequence[(
|
| 250 |
for (const opportunity of candidates) {
|
| 251 |
nextStates[opportunity.id] = 0;
|
| 252 |
}
|
|
@@ -555,9 +554,15 @@ function legendColor(letter: string) {
|
|
| 555 |
function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
|
| 556 |
const parsed = parseBridges(problemAscii);
|
| 557 |
const states = bridgesStatesFromBoard(problemAscii, boardAscii);
|
|
|
|
| 558 |
|
| 559 |
-
function cycle(opportunities: BridgesOpportunity[]) {
|
| 560 |
-
const nextStates = cycleBridgesOpportunities(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
onChange(bridgesBoardFromStates(problemAscii, nextStates));
|
| 562 |
}
|
| 563 |
|
|
@@ -571,7 +576,7 @@ function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzz
|
|
| 571 |
{parsed.lines.flatMap((line, r) =>
|
| 572 |
[...line].map((char, c) => {
|
| 573 |
const opportunities = parsed.cellToOpportunities.get(`${r}:${c}`) ?? [];
|
| 574 |
-
const boardChar =
|
| 575 |
const className = isClueChar(char)
|
| 576 |
? "board-cell fixed island-cell"
|
| 577 |
: opportunities.length > 0
|
|
@@ -584,7 +589,7 @@ function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzz
|
|
| 584 |
key={`${r}-${c}`}
|
| 585 |
type="button"
|
| 586 |
className={className}
|
| 587 |
-
onClick={() => cycle(opportunities)}
|
| 588 |
disabled={opportunities.length === 0}
|
| 589 |
>
|
| 590 |
{isClueChar(char) ? char : display}
|
|
|
|
| 106 |
return char === '"' ? 2 : char === "|" ? 1 : 0;
|
| 107 |
}
|
| 108 |
|
| 109 |
+
function bridgeOpportunitiesShareCell(a: BridgesOpportunity, b: BridgesOpportunity): boolean {
|
| 110 |
+
const bCells = new Set(b.cells.map(([r, c]) => `${r}:${c}`));
|
| 111 |
+
return a.cells.some(([r, c]) => bCells.has(`${r}:${c}`));
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
function parseBridges(problemAscii: string) {
|
| 115 |
const lines = splitLines(problemAscii);
|
| 116 |
const opportunities: BridgesOpportunity[] = [];
|
|
|
|
| 206 |
}
|
| 207 |
|
| 208 |
function cycleBridgesOpportunities(
|
| 209 |
+
parsed: ReturnType<typeof parseBridges>,
|
| 210 |
states: Record<string, 0 | 1 | 2>,
|
| 211 |
opportunities: BridgesOpportunity[],
|
| 212 |
+
clickedChar = ".",
|
| 213 |
): Record<string, 0 | 1 | 2> {
|
| 214 |
if (opportunities.length === 0) {
|
| 215 |
return states;
|
|
|
|
| 222 |
const opportunity = candidates[0];
|
| 223 |
const level = (((nextStates[opportunity.id] ?? 0) + 1) % 3) as 0 | 1 | 2;
|
| 224 |
nextStates[opportunity.id] = level;
|
| 225 |
+
if (level > 0) {
|
| 226 |
+
for (const other of parsed.opportunities) {
|
| 227 |
+
if (other.id !== opportunity.id && bridgeOpportunitiesShareCell(other, opportunity)) {
|
| 228 |
+
nextStates[other.id] = 0;
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
return nextStates;
|
| 233 |
}
|
| 234 |
|
|
|
|
| 239 |
]),
|
| 240 |
null,
|
| 241 |
];
|
| 242 |
+
const currentIndex =
|
| 243 |
+
sequence.findIndex(
|
| 244 |
+
(entry) => entry !== null && bridgeSymbol(entry.opportunity, entry.level) === clickedChar,
|
| 245 |
+
) ?? -1;
|
| 246 |
+
const normalizedCurrentIndex = currentIndex === -1 ? sequence.length - 1 : currentIndex;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
+
const nextEntry = sequence[(normalizedCurrentIndex + 1) % sequence.length];
|
| 249 |
for (const opportunity of candidates) {
|
| 250 |
nextStates[opportunity.id] = 0;
|
| 251 |
}
|
|
|
|
| 554 |
function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
|
| 555 |
const parsed = parseBridges(problemAscii);
|
| 556 |
const states = bridgesStatesFromBoard(problemAscii, boardAscii);
|
| 557 |
+
const boardLines = splitLines(boardAscii);
|
| 558 |
|
| 559 |
+
function cycle(opportunities: BridgesOpportunity[], r: number, c: number) {
|
| 560 |
+
const nextStates = cycleBridgesOpportunities(
|
| 561 |
+
parsed,
|
| 562 |
+
states,
|
| 563 |
+
opportunities,
|
| 564 |
+
boardLines[r]?.[c] ?? ".",
|
| 565 |
+
);
|
| 566 |
onChange(bridgesBoardFromStates(problemAscii, nextStates));
|
| 567 |
}
|
| 568 |
|
|
|
|
| 576 |
{parsed.lines.flatMap((line, r) =>
|
| 577 |
[...line].map((char, c) => {
|
| 578 |
const opportunities = parsed.cellToOpportunities.get(`${r}:${c}`) ?? [];
|
| 579 |
+
const boardChar = boardLines[r]?.[c] ?? char;
|
| 580 |
const className = isClueChar(char)
|
| 581 |
? "board-cell fixed island-cell"
|
| 582 |
: opportunities.length > 0
|
|
|
|
| 589 |
key={`${r}-${c}`}
|
| 590 |
type="button"
|
| 591 |
className={className}
|
| 592 |
+
onClick={() => cycle(opportunities, r, c)}
|
| 593 |
disabled={opportunities.length === 0}
|
| 594 |
>
|
| 595 |
{isClueChar(char) ? char : display}
|
frontend/test/pipeline/roundtrip.test.ts
CHANGED
|
@@ -124,13 +124,18 @@ describe("bridges generated click plans", () => {
|
|
| 124 |
throw new Error(`click index out of bounds: ${idx}`);
|
| 125 |
};
|
| 126 |
|
|
|
|
| 127 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 128 |
for (const idx of bridgesClickPlan[f.filename] ?? []) {
|
| 129 |
const [r, c] = indexToCoord(idx);
|
|
|
|
| 130 |
states = T.cycleBridgesOpportunities(
|
|
|
|
| 131 |
states,
|
| 132 |
parsed.cellToOpportunities.get(`${r}:${c}`) ?? [],
|
|
|
|
| 133 |
);
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
for (const opportunity of parsed.opportunities) {
|
|
@@ -166,27 +171,48 @@ describe("bridges ambiguous route cells", () => {
|
|
| 166 |
expect(vertical).toBeTruthy();
|
| 167 |
|
| 168 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 169 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 170 |
expect(states[horizontal!.id]).toBe(1);
|
| 171 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 172 |
|
| 173 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 174 |
expect(states[horizontal!.id]).toBe(2);
|
| 175 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 176 |
|
| 177 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 178 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 179 |
expect(states[vertical!.id]).toBe(1);
|
| 180 |
|
| 181 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 182 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 183 |
expect(states[vertical!.id]).toBe(2);
|
| 184 |
|
| 185 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 186 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 187 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 188 |
});
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
it("cycles a unique cell route independently", () => {
|
| 191 |
const parsed = T.parseBridges(crossingProblem);
|
| 192 |
const centerCandidates = parsed.cellToOpportunities.get("2:2") ?? [];
|
|
@@ -201,10 +227,10 @@ describe("bridges ambiguous route cells", () => {
|
|
| 201 |
expect(vertical).toBeTruthy();
|
| 202 |
|
| 203 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 204 |
-
states = T.cycleBridgesOpportunities(states, centerCandidates);
|
| 205 |
-
states = T.cycleBridgesOpportunities(states, verticalCandidates);
|
| 206 |
|
| 207 |
-
expect(states[horizontal!.id]).toBe(
|
| 208 |
expect(states[vertical!.id]).toBe(1);
|
| 209 |
});
|
| 210 |
|
|
|
|
| 124 |
throw new Error(`click index out of bounds: ${idx}`);
|
| 125 |
};
|
| 126 |
|
| 127 |
+
let board = f.problem;
|
| 128 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 129 |
for (const idx of bridgesClickPlan[f.filename] ?? []) {
|
| 130 |
const [r, c] = indexToCoord(idx);
|
| 131 |
+
const clickedChar = T.splitLines(board)[r]?.[c] ?? ".";
|
| 132 |
states = T.cycleBridgesOpportunities(
|
| 133 |
+
parsed,
|
| 134 |
states,
|
| 135 |
parsed.cellToOpportunities.get(`${r}:${c}`) ?? [],
|
| 136 |
+
clickedChar,
|
| 137 |
);
|
| 138 |
+
board = T.bridgesBoardFromStates(f.problem, states);
|
| 139 |
}
|
| 140 |
|
| 141 |
for (const opportunity of parsed.opportunities) {
|
|
|
|
| 171 |
expect(vertical).toBeTruthy();
|
| 172 |
|
| 173 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 174 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, ".");
|
| 175 |
expect(states[horizontal!.id]).toBe(1);
|
| 176 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 177 |
|
| 178 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, "-");
|
| 179 |
expect(states[horizontal!.id]).toBe(2);
|
| 180 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 181 |
|
| 182 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, "=");
|
| 183 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 184 |
expect(states[vertical!.id]).toBe(1);
|
| 185 |
|
| 186 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, "|");
|
| 187 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 188 |
expect(states[vertical!.id]).toBe(2);
|
| 189 |
|
| 190 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, "\"");
|
| 191 |
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 192 |
expect(states[vertical!.id] ?? 0).toBe(0);
|
| 193 |
});
|
| 194 |
|
| 195 |
+
it("cycles a shared cell through all visible states after each board render", () => {
|
| 196 |
+
const parsed = T.parseBridges(crossingProblem);
|
| 197 |
+
const centerCandidates = parsed.cellToOpportunities.get("2:2") ?? [];
|
| 198 |
+
let board = crossingProblem;
|
| 199 |
+
const visible: string[] = [];
|
| 200 |
+
|
| 201 |
+
for (let i = 0; i < 5; i += 1) {
|
| 202 |
+
const states = T.bridgesStatesFromBoard(crossingProblem, board);
|
| 203 |
+
const nextStates = T.cycleBridgesOpportunities(
|
| 204 |
+
parsed,
|
| 205 |
+
states,
|
| 206 |
+
centerCandidates,
|
| 207 |
+
T.splitLines(board)[2]?.[2] ?? ".",
|
| 208 |
+
);
|
| 209 |
+
board = T.bridgesBoardFromStates(crossingProblem, nextStates);
|
| 210 |
+
visible.push(T.splitLines(board)[2]?.[2] ?? ".");
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
expect(visible).toEqual(["-", "=", "|", "\"", "."]);
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
it("cycles a unique cell route independently", () => {
|
| 217 |
const parsed = T.parseBridges(crossingProblem);
|
| 218 |
const centerCandidates = parsed.cellToOpportunities.get("2:2") ?? [];
|
|
|
|
| 227 |
expect(vertical).toBeTruthy();
|
| 228 |
|
| 229 |
let states: Record<string, 0 | 1 | 2> = {};
|
| 230 |
+
states = T.cycleBridgesOpportunities(parsed, states, centerCandidates, ".");
|
| 231 |
+
states = T.cycleBridgesOpportunities(parsed, states, verticalCandidates, ".");
|
| 232 |
|
| 233 |
+
expect(states[horizontal!.id] ?? 0).toBe(0);
|
| 234 |
expect(states[vertical!.id]).toBe(1);
|
| 235 |
});
|
| 236 |
|