Skip to content

Commit

Permalink
Simple Anagram Helper (#1844)
Browse files Browse the repository at this point in the history
* Simple Anagram Helper

---------

Co-authored-by: Alex Sanders <[email protected]>
  • Loading branch information
oliverabrahams and sndrs authored Dec 10, 2024
1 parent ea2e0c3 commit 33cbe41
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 505 deletions.
8 changes: 7 additions & 1 deletion libs/@guardian/react-crossword/.storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<style>
/* BEGIN HEADLINE */

@font-face {
font-family: 'GH Guardian Headline';
src:
Expand Down Expand Up @@ -383,4 +382,11 @@
font-weight: 700;
font-style: normal;
}
*,
*::before,
*::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
</style>
1 change: 1 addition & 0 deletions libs/@guardian/react-crossword/src/@types/crossword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type Theme = {
foreground: string;
anagramHelperBackground: string;
text: string;
provisionalText: string;
errorText: string;
gutter: number;
highlight: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { groupedClues as data } from '../../stories/formats/grouped-clues';
import { progress12Across } from '../../stories/formats/grouped-clues.progress';
import { ContextProvider } from '../context/ContextProvider';
import { AnagramHelper } from './AnagramHelper';

Expand All @@ -8,7 +9,11 @@ const meta: Meta<typeof AnagramHelper> = {
title: 'Components/Anagram Helper',
decorators: [
(Story) => (
<ContextProvider data={data} selectedEntryId="12-across">
<ContextProvider
data={data}
selectedEntryId="12-across"
userProgress={progress12Across}
>
<Story />
</ContextProvider>
),
Expand Down
214 changes: 99 additions & 115 deletions libs/@guardian/react-crossword/src/components/AnagramHelper.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,77 @@
import { css } from '@emotion/react';
import { space } from '@guardian/source/foundations';
import { SvgCross } from '@guardian/source/react-components';
import { useCallback, useEffect, useState } from 'react';
import { SvgCross, TextInput } from '@guardian/source/react-components';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCurrentClue } from '../context/CurrentClue';
import { useData } from '../context/Data';
import { useProgress } from '../context/Progress';
import { useTheme } from '../context/Theme';
import { useUIState } from '../context/UI';
import { useUpdateCell } from '../hooks/useUpdateCell';
import type { AnagramHelperProgress } from '../utils/getAnagramHelperProgressForGroup';
import { getAnagramHelperProgressForGroup } from '../utils/getAnagramHelperProgressForGroup';
import { biasedShuffle } from '../utils/biasedShuffle';
import { getCellsWithProgressForGroup } from '../utils/getCellsWithProgressForGroup';
import { Button } from './Button';
import { Clue } from './Clue';
import { SolutionDisplay } from './SolutionDisplay';
import { SolutionDisplayKey } from './SolutionDisplayKey';
import { WordWheel } from './WordWheel';

const inputRegex = /[^A-Za-zÀ-ÿ0-9]/g;

export const AnagramHelper = () => {
const [shuffled, setShuffled] = useState<boolean>(false);
const [candidateLetters, setCandidateLetters] = useState<string[]>([]);
const [wordWheelLetters, setWordWheelLetters] = useState<string[]>([]);
const [progressLetters, setProgressLetters] = useState<
AnagramHelperProgress[]
>([]);
const { entries } = useData();
const { progress } = useProgress();
const { updateCell } = useUpdateCell();
const [letters, setLetters] = useState('');
const [solving, setSolving] = useState(false);
const [shuffledLetters, setShuffledLetters] = useState<string[]>([]);
const theme = useTheme();
const { setShowAnagramHelper } = useUIState();
const { entries, cells } = useData();
const { currentEntryId } = useCurrentClue();
const entry = currentEntryId ? entries.get(currentEntryId) : undefined;
const { progress } = useProgress();

const reset = useCallback(() => {
const progressLetters = getAnagramHelperProgressForGroup({
const entry = useMemo(() => {
return currentEntryId ? entries.get(currentEntryId) : undefined;
}, [currentEntryId, entries]);

const cellsWithProgress = useMemo(() => {
return getCellsWithProgressForGroup({
entry,
cells,
entries,
progress,
});
setProgressLetters(
getAnagramHelperProgressForGroup({ entry, entries, progress }),
);
setCandidateLetters(
Array.from({ length: progressLetters.length }, () => ''),
);
setShuffled(false);
}, [entries, entry, progress]);
}, [entry, cells, entries, progress]);

const save = useCallback(() => {
for (const progressLetter of progressLetters) {
if (!progressLetter.isSaved) {
updateCell({
...progressLetter.coords,
value: progressLetter.progress,
});
}
}
}, [progressLetters, updateCell]);
const reset = useCallback(() => {
setShuffledLetters([]);
setSolving(false);
}, []);

const shuffle = useCallback(() => {
setShuffled(true);
setCandidateLetters((prevState) => {
const shuffleLetters = [...prevState];
const matchedLetters = Array.from(
{ length: progressLetters.length },
() => '',
);
// remove letters that exist in progressLetters but only the number of times they exist
progressLetters.forEach((groupProgress, index) => {
const shuffleLetterIndex = shuffleLetters.indexOf(
groupProgress.progress,
);
if (shuffleLetterIndex !== -1) {
matchedLetters[index] =
shuffleLetters.splice(shuffleLetterIndex, 1)[0] ?? '';
}
});
setShuffledLetters(biasedShuffle(letters.split('')));
}, [letters]);

// shuffle the candidate letters and remove blanks
shuffleLetters
.sort(() => Math.random() - 0.5)
.filter((shuffleLetter) => shuffleLetter !== '');
const start = useCallback(() => {
shuffle();
setSolving(true);
}, [shuffle]);

return matchedLetters.map((letter) => {
if (letter === '') {
return shuffleLetters.pop() ?? '';
}
return letter;
});
});
const newWordWheelLetters = [...candidateLetters]
.filter((letter) => !!letter)
.sort(() => Math.random() - 0.5);
setWordWheelLetters(newWordWheelLetters);
}, [candidateLetters, progressLetters]);

//initialise the candidate letters and progress letters
useEffect(() => {
reset();
}, [reset]);

return (
<div
css={css`
position: fixed;
overflow: auto;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-direction: column;
background-color: ${theme.anagramHelperBackground};
padding: 10px;
min-height: fit-content;
z-index: 2;
`}
>
<div
Expand All @@ -132,56 +97,75 @@ export const AnagramHelper = () => {
flex-direction: column;
`}
>
<div
css={css`
display: flex;
width: 100%;
justify-content: center;
`}
>
<WordWheel letters={wordWheelLetters} />
</div>
<div
css={css`
> * {
margin: 0 ${space[1]}px;
}
`}
>
<Button onSuccess={shuffle} priority="primary">
shuffle
</Button>
<Button
onSuccess={save}
priority="secondary"
requireConfirmation={true}
>
save
</Button>
<Button
onSuccess={reset}
priority="secondary"
requireConfirmation={true}
{!solving && (
<div
css={css`
display: grid;
justify-items: center;
grid-template-columns: 1fr auto;
`}
>
reset
</Button>
</div>
<div>
<TextInput
hideLabel={true}
label="Enter letters"
spellCheck="false"
onChange={(event) => {
const letters = event.target.value.replace(inputRegex, '');
setLetters(letters.toUpperCase());
}}
value={letters}
maxLength={cellsWithProgress.length}
/>
</div>
<Button
cssOverrides={css`
margin: ${space[1]}px 0 0 ${space[1]}px;
`}
onSuccess={start}
disabled={letters.length < 1}
priority="primary"
size="default"
>
Start
</Button>
<span>
{letters.length}/{cellsWithProgress.length}
</span>
</div>
)}
{solving && (
<>
<WordWheel letters={shuffledLetters} />
<div
css={css`
margin: ${space[4]}px 0 0;
> * {
margin: 0 ${space[1]}px;
}
`}
>
<Button onSuccess={reset} size={'default'} priority="secondary">
Back
</Button>
<Button onSuccess={shuffle} size={'default'} priority="primary">
Shuffle
</Button>
</div>
</>
)}
<div
css={css`
margin-top: 10px;
width: 100%;
margin: ${space[4]}px 0 ${space[4]}px;
border-top: 1px solid ${theme.background};
`}
>
{entry && <Clue entry={entry} />}
</div>
/>
{entry && <Clue entry={entry} />}
<SolutionDisplay
shuffled={shuffled}
setShuffled={setShuffled}
candidateLetters={candidateLetters}
setCandidateLetters={setCandidateLetters}
setProgressLetters={setProgressLetters}
progressLetters={progressLetters}
cellsWithProgress={cellsWithProgress}
shuffledLetters={shuffledLetters}
/>
<SolutionDisplayKey />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,22 @@ type Story = StoryObj<typeof SolutionDisplay>;

export const Default: Story = {
args: {
candidateLetters: ['T', 'E', '', ''],
progressLetters: [
cellsWithProgress: [
{
coords: { x: 0, y: 0 },
x: 0,
y: 0,
progress: 'T',
isSaved: true,
separator: ',',
},
{ coords: { x: 1, y: 0 }, progress: 'E', isSaved: true },
{ x: 1, y: 0, progress: 'E' },
{
coords: { x: 2, y: 0 },
x: 2,
y: 0,
progress: 'S',
isSaved: true,
separator: '-',
},
{ coords: { x: 3, y: 0 }, progress: '', isSaved: true },
{ x: 3, y: 0, progress: '' },
],
setCandidateLetters: () => {},
setProgressLetters: () => {},
shuffledLetters: ['T', 'E', 'S', 'T'],
},
};
Loading

0 comments on commit 33cbe41

Please sign in to comment.