Skip to content

Commit

Permalink
Add hard limit to the number of possible solutions to allow
Browse files Browse the repository at this point in the history
  • Loading branch information
mdirolf committed Nov 20, 2024
1 parent c36601a commit 43de648
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 19 deletions.
12 changes: 6 additions & 6 deletions app/__tests__/puzzleReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test('check closest alt', () => {
],
[[0, 'A']],
]
)
)[0]
)
).toMatchInlineSnapshot(`
[
Expand All @@ -49,7 +49,7 @@ test('check closest alt', () => {
],
[[0, 'A']],
]
)
)[0]
)
).toMatchInlineSnapshot(`
[
Expand All @@ -74,7 +74,7 @@ test('check closest alt', () => {
[1, 'B'],
],
]
)
)[0]
)
).toMatchInlineSnapshot(`
[
Expand All @@ -99,7 +99,7 @@ test('check closest alt', () => {
[1, 'G'],
],
]
)
)[0]
)
).toMatchInlineSnapshot(`
[
Expand Down Expand Up @@ -138,7 +138,7 @@ test('check without alt', () => {
revealedCells: new Set(),
wrongCells: new Set(),
grid,
solutions: allSolutions(dbpuz.g, []),
solutions: allSolutions(dbpuz.g, [])[0],
cellsIterationCount: [],
cellsEverMarkedWrong: new Set(),
cellsUpdatedAt: [],
Expand Down Expand Up @@ -187,7 +187,7 @@ test('check with alt', () => {
revealedCells: new Set(),
wrongCells: new Set(),
grid,
solutions: allSolutions(dbpuz.g, [[[0, 'M']]]),
solutions: allSolutions(dbpuz.g, [[[0, 'M']]])[0],
cellsIterationCount: [],
cellsEverMarkedWrong: new Set(),
cellsUpdatedAt: [],
Expand Down
199 changes: 196 additions & 3 deletions app/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,8 @@ cases(
cases(
'checkGrid',
(opts) => {
expect(checkGrid(opts.grid, allSolutions(opts.answers, opts.alts))).toEqual(
opts.res
);
const solutions = allSolutions(opts.answers, opts.alts);
expect(checkGrid(opts.grid, solutions[0])).toEqual(opts.res);
},
[
{ grid: [], answers: [], alts: [], res: [true, true] },
Expand Down Expand Up @@ -357,5 +356,199 @@ cases(
alts: [[[3, 'E'] as [number, string]]],
res: [true, false],
},
{
answers: [
'E',
'R',
'I',
'C',
'B',
'E',
'E',
'N',
'O',
'N',
'E',
'R',
'R',
'U',
'C',
'L',
'I',
'I',
'I',
'D',
'E',
'A',
'N',
'C',
'C',
'S',
'U',
'C',
'K',
'B',
'B',
'E',
'R',
'I',
'C',
'B',
],
grid: [
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
],
alts: [
{ '0': 'WHEN' },
{ '0': 'WHEN', '1': 'I' },
{ '0': 'WHEN', '1': 'I', '2': 'FIRST' },
{ '0': 'WHEN', '1': 'I', '2': 'FIRST', '3': 'MET' },
{ '0': 'WHEN', '1': 'I', '2': 'FIRST', '3': 'MET', '4': 'PAIN' },
{ '5': 'IT' },
{ '5': 'IT', '11': 'WAS' },
{ '5': 'IT', '11': 'WAS', '17': 'A' },
{ '5': 'IT', '11': 'WAS', '17': 'A', '23': 'HOME' },
{ '5': 'IT', '11': 'WAS', '17': 'A', '23': 'HOME', '29': 'I' },
{ '6': 'RETURNED' },
{ '6': 'RETURNED', '12': 'TO' },
{ '6': 'RETURNED', '12': 'TO', '18': 'IT' },
{ '6': 'RETURNED', '12': 'TO', '18': 'IT', '24': 'WHENEVER' },
{
'6': 'RETURNED',
'12': 'TO',
'18': 'IT',
'24': 'WHENEVER',
'30': 'I',
},
{ '31': 'AM' },
{ '31': 'AM', '32': 'CALLED' },
{ '31': 'AM', '32': 'CALLED', '33': 'BACK' },
{ '31': 'AM', '32': 'CALLED', '33': 'BACK', '34': 'TO' },
{
'31': 'AM',
'32': 'CALLED',
'33': 'BACK',
'34': 'TO',
'35': 'IT',
},
{ '7': 'I' },
{ '7': 'I', '8': 'H', '9': 'O', '10': 'P', '13': 'E' },
{ '7': 'I', '8': 'H', '9': 'O', '10': 'P', '13': 'E', '14': 'I' },
{
'7': 'I',
'8': 'H',
'9': 'O',
'10': 'P',
'13': 'E',
'14': 'I',
'15': 'D',
'19': 'E',
},
{
'7': 'I',
'8': 'H',
'9': 'O',
'10': 'P',
'13': 'E',
'14': 'I',
'15': 'D',
'19': 'E',
'20': 'F',
'22': 'R',
},
{
'7': 'I',
'8': 'H',
'9': 'O',
'10': 'P',
'13': 'E',
'14': 'I',
'15': 'D',
'19': 'E',
'20': 'F',
'22': 'R',
'25': 'F',
'26': 'R',
'27': 'O',
'28': 'M',
},
{
'0': 'H',
'1': 'H',
'2': 'O',
'3': 'M',
'4': 'E',
'5': 'O',
'6': 'H',
'7': 'I',
'8': 'H',
'9': 'O',
'10': 'P',
'11': 'H',
'12': 'O',
'13': 'E',
'14': 'I',
'15': 'D',
'17': 'O',
'18': 'M',
'19': 'E',
'20': 'F',
'22': 'R',
'23': 'M',
'24': 'E',
'25': 'F',
'26': 'R',
'27': 'O',
'28': 'M',
'29': 'E',
'30': 'M',
'31': 'H',
'32': 'O',
'33': 'M',
'34': 'E',
'35': 'E',
},
].map((alt) => {
return Object.entries(alt).map(
(e) => [Number(e[0]), e[1]] as [number, string]
);
}),
res: [false, false],
},
]
);
17 changes: 16 additions & 1 deletion app/components/ClueMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
directionString,
removeClueSpecials,
} from '../lib/types.js';
import { isMetaSolution } from '../lib/utils.js';
import { allSolutions, isMetaSolution } from '../lib/utils.js';
import { addClues } from '../lib/viewableGrid.js';
import {
AddAlternateAction,
Expand Down Expand Up @@ -263,6 +263,21 @@ export const ClueMode = ({ state, ...props }: ClueModeProps) => {
<AlternateSolutionEditor
grid={state.grid.cells}
save={(alt) => {
if (
allSolutions(
state.grid.cells,
[...state.alternates, alt].map((alt) => {
return Object.entries(alt).map(
(e) => [Number(e[0]), e[1]] as [number, string]
);
})
)[1]
) {
showSnackbar(
'Could not add alternate - too many possible solutions specified!'
);
return Promise.resolve();
}
const act: AddAlternateAction = {
type: 'ADDALT',
alternate: alt,
Expand Down
2 changes: 1 addition & 1 deletion app/components/Puzzle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export const Puzzle = ({
showExtraKeyLayout: false,
answers: puzzle.grid,
alternateSolutions: puzzle.alternateSolutions,
solutions: allSolutions(puzzle.grid, puzzle.alternateSolutions),
solutions: allSolutions(puzzle.grid, puzzle.alternateSolutions)[0],
verifiedCells: new Set<number>(play ? play.vc : []),
wrongCells: new Set<number>(play ? play.wc : []),
revealedCells: new Set<number>(play ? play.rc : []),
Expand Down
2 changes: 1 addition & 1 deletion app/lib/serverOnly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ export const getPuzzlePageProps: GetServerSideProps<PuzzlePageProps> = async ({
);
const clueMap = getEntryToClueMap(
grid,
allSolutions(fromDB.grid, fromDB.alternateSolutions)
allSolutions(fromDB.grid, fromDB.alternateSolutions)[0]
);
const puzzle: PuzzleResultWithAugmentedComments = {
...fromDB,
Expand Down
18 changes: 16 additions & 2 deletions app/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,18 @@ export function isMetaSolution(
return false;
}

const MAX_SOLUTIONS = 200;
/* Given the answer key and alts return an array of all possible solutions.
*
* When alts are non-overlapping the number of possible solutions can explode.
* We cap the number of possibilities and return a flag indicating if we reached
* the cap. The UI can use that flag to warn / disable adding more alts. */
export function allSolutions(
answer: string[],
alts: [index: number, value: string][][]
): NonEmptyArray<string[]> {
): [NonEmptyArray<string[]>, boolean] {
const combos: NonEmptyArray<[index: number, value: string][]> = [[]];
let reachedLimit = false;

for (const alt of alts) {
for (const combo of [...combos]) {
Expand All @@ -130,8 +137,15 @@ export function allSolutions(
const newCombo = [...combo, ...alt];
const uniq = [...new Map(newCombo.map((v) => [v[0], v])).values()];
combos.push(uniq);
if (combos.length >= MAX_SOLUTIONS) {
reachedLimit = true;
break;
}
}
}
if (reachedLimit) {
break;
}
}

function comboToSoln(alt: [index: number, value: string][]) {
Expand All @@ -143,7 +157,7 @@ export function allSolutions(
}

const [head, ...rest] = combos;
return [comboToSoln(head), ...rest.map(comboToSoln)];
return [[comboToSoln(head), ...rest.map(comboToSoln)], reachedLimit];
}

export function checkGrid(
Expand Down
Loading

0 comments on commit 43de648

Please sign in to comment.