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 isSequenceMatch = (entry: (typeof sequence)[number]) =>
229
- candidates.every((opportunity) => {
230
- const expected = entry?.opportunity.id === opportunity.id ? entry.level : 0;
231
- return (nextStates[opportunity.id] ?? 0) === expected;
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[(currentIndex + 1) % sequence.length];
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(states, opportunities);
 
 
 
 
 
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 = splitLines(boardAscii)[r]?.[c] ?? char;
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(1);
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