diff --git a/backend/app/analysis/anagrams.py b/backend/app/analysis/anagrams.py index b1c55c8..94bb98b 100644 --- a/backend/app/analysis/anagrams.py +++ b/backend/app/analysis/anagrams.py @@ -2,6 +2,7 @@ Helper functions for generating and checking anagrams """ import os +import random from django.conf import settings @@ -15,6 +16,16 @@ def get_word_set(): return word_set +def scramble_word(word): + """ + :param word: the word to scramble for single word anagrams + :return: a string of the word with the letters scrambled + """ + word_as_list = list(word) + random.shuffle(word_as_list) + return ''.join(word_as_list) + + def get_letter_freq(letters): """ Given a word, find the frequency of letters in this word diff --git a/backend/app/views.py b/backend/app/views.py index ed23e05..f6c1c30 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -17,12 +17,13 @@ TextSerializer ) from .analysis.parts_of_speech import ( - filter_pos, + get_part_of_speech_words, get_valid_words, ) from .analysis.anagrams import ( - get_anagrams, get_letter_freq, + get_word_set, + scramble_word, ) from .analysis.textdata import ( get_text_data, @@ -100,10 +101,6 @@ def get_anagram(request, text_id, part_of_speech): elif anagram_freq[letter] < cur_freq[letter]: anagram_freq[letter] = cur_freq[letter] - extra_words = get_anagrams(anagram_freq) - extra_words -= set(words) # Remove words from text from extra words - extra_words = filter_pos(extra_words, part_of_speech) - scrambled_letters = [] for letter in anagram_freq: for i in range(anagram_freq[letter]): @@ -113,13 +110,25 @@ def get_anagram(request, text_id, part_of_speech): 'letters': scrambled_letters, 'word_data': [{'word': word, 'definition': definitions[word].get(part_of_speech, []), - 'example': examples[word].get(part_of_speech, [])} - for word in words], - 'extra_words': extra_words + 'example': examples[word].get(part_of_speech, []), + 'scrambled': scramble_word(word)} + for word in words] } return Response(res) +word_set = get_word_set() + + +@api_view(['GET']) +def check_word(request, word, pos): + """ + API endpoint for checking whether an extra word is part of the extra word set + """ + words = get_part_of_speech_words(word, pos) # Used to check for part of speech in extra words + return Response(len(word) >= 3 and len(words) > 0 and word in word_set) + + @api_view(['GET']) def get_picturebook_prompt(request, text_id, part_of_speech): """ diff --git a/backend/config/urls.py b/backend/config/urls.py index fbdab93..e1ce18c 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -20,6 +20,7 @@ from app.views import ( all_text, get_anagram, + check_word, update_text, delete_text, add_text, @@ -51,6 +52,7 @@ def react_view_path(route, component_name): path('api/all_text', all_text), path('api/all_text/', all_text), path('api/get_anagram//', get_anagram), + path('api/check_word//', check_word), path('api/update_text', update_text), path('api/delete_text', delete_text), path('api/add_text', add_text), diff --git a/frontend/src/UILibrary/styles.scss b/frontend/src/UILibrary/styles.scss index cdd7353..34217ec 100644 --- a/frontend/src/UILibrary/styles.scss +++ b/frontend/src/UILibrary/styles.scss @@ -66,3 +66,13 @@ body { text-align: center; height: 86%; } + +.loading-spinner { + margin: 0 auto auto; +} + +.loading-text { + margin: auto auto 30px; + font-size: 30px; + font-weight: bold; +} diff --git a/frontend/src/anagramView/anagramView.js b/frontend/src/anagramView/anagramView.js index daed788..498654b 100644 --- a/frontend/src/anagramView/anagramView.js +++ b/frontend/src/anagramView/anagramView.js @@ -5,6 +5,7 @@ import * as PropTypes from 'prop-types'; import { Navbar, Footer, LoadingPage } from '../UILibrary/components'; +// Configurations for Confetti library const CONFETTI_CONFIG = { angle: 90, spread: 70, @@ -43,30 +44,32 @@ const generateFreq = (word) => { return freq; }; +// Modes +const UNSELECTED = 0; +const SINGLE = 1; +const COMBINED = 2; + export class AnagramView extends React.Component { constructor(props) { super(props); this.state = { - targetWordDefs: null, - targetWords: [], - extraWords: [], - targetExamples: [], - userInput: '', - targetWordsFound: [], extraWordsFound: [], - score: 0, - letters: [], gameOver: false, - timeLeft: 90, - showModal: false, + letters: [], + mode: UNSELECTED, + score: 0, shake: false, showConfetti: false, + showModal: false, + scrambled: [], + targetExamples: [], + targetWords: [], + targetWordDefs: null, + targetWordsFound: [], + timeLeft: 90, + userInput: '', }; this.timer = 0; - this.startTimer = this.startTimer.bind(this); - this.pauseTimer = this.pauseTimer.bind(this); - this.countDown = this.countDown.bind(this); - this.modalHandler = this.modalHandler.bind(this); } async componentDidMount() { @@ -92,12 +95,17 @@ export class AnagramView extends React.Component { }); }; - handleSubmit = (event) => { + checkExtraWord = async (word) => { + const apiURL = `/api/check_word/${word}/${this.props.partOfSpeech}`; + const response = await fetch(apiURL); + return response.json(); + } + + handleSubmit = async (event) => { event.preventDefault(); const userInput = this.state.userInput.toLowerCase().trim(); const targetWords = this.state.targetWords.map((word) => (word.toLowerCase())); const targetWordsFound = this.state.targetWordsFound; - const extraWords = this.state.extraWords; if (targetWords.includes(userInput) && !targetWordsFound.includes(userInput)) { this.setState({ showConfetti: true, @@ -109,12 +117,18 @@ export class AnagramView extends React.Component { gameOver: true, }); } - } else if (extraWords.has(userInput) && !this.state.extraWordsFound.includes(userInput)) { - this.setState({ - showConfetti: true, - extraWordsFound: this.state.extraWordsFound.concat(userInput), - score: this.state.score + userInput.length, - }); + } else if (!targetWords.includes(userInput) + && !this.state.extraWordsFound.includes(userInput)) { + const isWord = await this.checkExtraWord(userInput); + if (isWord) { + this.setState({ + showConfetti: false, + extraWordsFound: this.state.extraWordsFound.concat(userInput), + score: this.state.score + userInput.length, + }); + } else { + this.setState({ shake: true }); + } } else { this.setState({ shake: true }); } @@ -135,8 +149,7 @@ export class AnagramView extends React.Component { this.timer = 0; }; - modalHandler = (event) => { - event.preventDefault(); + modalHandler = () => { if (this.state.showModal) this.startTimer(); else this.pauseTimer(); this.setState({ @@ -144,21 +157,23 @@ export class AnagramView extends React.Component { }); } - reset() { + reset = () => { this.setState({ - targetWordDefs: null, - targetWords: [], - extraWords: [], - targetExamples: [], - userInput: '', - targetWordsFound: [], extraWordsFound: [], - score: 0, - letters: [], gameOver: false, - timeLeft: 90, + letters: [], + mode: UNSELECTED, + score: 0, + scrambled: [], shake: false, showConfetti: false, + showModal: false, + targetExamples: [], + targetWords: [], + targetWordDefs: null, + targetWordsFound: [], + timeLeft: 90, + userInput: '', }); this.timer = 0; } @@ -173,6 +188,7 @@ export class AnagramView extends React.Component { let letters = []; const targetWordDefs = []; const targetExamples = []; + const scrambled = []; const wordData = data['word_data']; for (let i = 0; i < wordData.length; i++) { const curData = wordData[i]; @@ -180,6 +196,7 @@ export class AnagramView extends React.Component { const examples = curData['example']; targetWords.push(word); targetWordDefs.push(curData['definition']); + scrambled.push(curData['scrambled']); if (examples.length === 0) { targetExamples.push(['N/A']); } else { @@ -189,16 +206,14 @@ export class AnagramView extends React.Component { for (let i = 0; i < (data['letters']).length; i++) { letters.push(data['letters'][i].toUpperCase()); } - const extraWordsSet = new Set(data['extra_words']); letters = shuffleArray(letters); this.setState({ - targetWordDefs: targetWordDefs, - extraWords: extraWordsSet, - targetWords: targetWords, - letters: letters, - targetExamples: targetExamples, + targetWordDefs, + targetWords, + letters, + targetExamples, + scrambled, }); - this.startTimer(); } catch (e) { console.log(e); } @@ -229,279 +244,391 @@ export class AnagramView extends React.Component { } } - render() { - if (!this.state.targetWordDefs) { - return (); - } + /** + * Render Methods + */ - /* - * Words Found - */ - const wordsFound = this.state.targetWords.map((word, i) => { - if (this.state.gameOver) { - return ( -
-
  • - - {word.toUpperCase()} - -
  • - - Examples: -
      - { - this.state.targetExamples[i] - .map((ex, j) => ( -
    1. - {ex} -
    2. - )) - } -
    -
    -
    - ); - } - if (this.state.targetWordsFound.includes(word.toLowerCase())) { - return ( -
    -
  • - - {word.toUpperCase()} - -
  • - - Examples: -
      - { - this.state.targetExamples[i] - .map((ex, j) => { - return ( -
    1. - {ex} -
    2. - ); - }) - } -
    -
    -
    - ); - } - let buffer = ''; - for (let j = 0; j < word.length; j++) { - buffer += '_ '; - } + renderWordsFound = () => this.state.targetWords.map((word, i) => { + if (this.state.gameOver) { return (
  • - {buffer} + + {word.toUpperCase()} +
  • - Examples: +

    Examples of Word Usage:

      { this.state.targetExamples[i] - .map((ex, j) => { - return ( -
    1. - {ex.replaceAll(word, buffer)} -
    2. - ); - }) + .map((ex, j) => ( +
    3. + {ex} +
    4. + )) }
    ); - }); - - /* - * Definitions - */ - const definitions = this.state.targetWordDefs.map((defs, i) => { - if (defs.length === 0) { - return ( -
  • - N/A -
  • - ); - } + } + if (this.state.targetWordsFound.includes(word.toLowerCase())) { return (
    -
  • - {defs[0]} +
  • + + {word.toUpperCase()} +
  • - - Definitions: + +

    Examples of Word Usage:

      { - defs.map((def, j) => ( -
    1. - {def} -
    2. - )) + this.state.targetExamples[i] + .map((ex, j) => { + return ( +
    3. + {ex} +
    4. + ); + }) }
    ); - }); + } + let buffer = ''; + for (let j = 0; j < word.length; j++) { + buffer += '_ '; + } + return ( +
    +
  • + {buffer} +
  • + +

    Examples of Word Usage:

    +
      + { + this.state.targetExamples[i] + .map((ex, j) => { + return ( +
    1. + {ex.replaceAll(word, buffer)} +
    2. + ); + }) + } +
    +
    +
    + ); + }); - const enteredFreq = generateFreq(this.state.userInput); - const curFreq = {}; - for (const key of Object.keys(enteredFreq)) { - curFreq[key] = 0; + renderDefinitions = () => this.state.targetWordDefs.map((defs, i) => { + if (defs.length === 0) { + return ( +
  • + N/A +
  • + ); } - return ( - -
    -
    -
    -

    - Anagrams - -

    -
    -
    + return ( +
    +
  • + {defs[0]} +
  • + +

    Extra Definitions:

    +
      { - this.state.showModal - ?
      -
      - : null + defs.map((def, j) => ( +
    1. + {def} +
    2. + )) } -
      -
      -
      Instructions
      - -
      -
      -

      Instructions will go here.

      -
      -
      - -
      -
      -
    -
    -

    Time Left: {this.state.timeLeft}

    -
    + + +
    + ); + }); + + renderInstructionModal = () =>
    + { + this.state.showModal + ?
    -

    Category: {this.props.partOfSpeech}

    + : null + } +
    +
    +
    Instructions
    + +
    +
    +

    With the list of given letters, rearrange them (or some of them) + to form words from the text you selected. To enter the words, + type in the slot that says "Type Here" then press enter, and + your score is shown on the top right corner in the green box + labeled "Score". To shuffle the letters, click on the button + to the left of the list of letters. You may also look at the + "Definitions" column as a hint. If you find a word that was + not part of the text, it will appear on the "Extra + Words" column. When you hover over the words in "Target Words", + you can view example sentences that may help with better + understanding the vocabulary.

    +

    To give up and view all the target words, click on the "Give Up" + button. To restart the game again, click on the "Restart" button. + The game will end if either the timer runs out or if you have + found all the target words.

    +
    +
    + +
    +
    +
    ; + + renderAnagramHeader = () => <> +
    +
    +

    + Anagrams + +

    +

    + Category: {this.props.partOfSpeech} +

    +
    + { this.renderInstructionModal() } +
    +

    Time Left: {this.state.timeLeft}

    +
    +
    + + + renderHeader = () => <> + { + this.state.gameOver + ?
    + Congratulations! You found + {this.state.targetWordsFound.length} + of the target words! + Click restart to start a new game! +
    + : null + } +
    +
    + + +
    +
    + Score: {this.state.score} +
    +
    + ; + + renderExtraWords = () => <> +

    Extra Words

    +
      + { + this.state.extraWordsFound.map((word, i) => ( +
    • + {word.toUpperCase()} +
    • + )) + } +
    + ; + + renderScrambled = () => <> +

    Scrambed Words (Click One)

    +
      + { + this.state.scrambled.map((scrambledWord, k) => ( +
    1. { this.setState({ letters: [...scrambledWord] }); }} + key={k} + > + { scrambledWord } +
    2. + )) + } +
    + ; + + renderAnagramInfo = () =>
    +
    +
    { - this.state.gameOver - ?
    - Congratulations! You found - {this.state.targetWordsFound.length} - of the target words! - Click restart to start a new game! -
    - : null + this.state.mode === SINGLE + ? this.renderScrambled() + : this.renderExtraWords() } -
    -
    - - -
    -
    - Score: {this.state.score} -
    +
    +
    +
    +
    +

    Words Found

    +
      {this.renderWordsFound()}
    +
    +
    +
    +
    +

    Definitions

    + +
      {this.renderDefinitions()}
    +
    +
    +
    ; + + renderInput = () => { + const enteredFreq = generateFreq(this.state.userInput); + const curFreq = {}; + for (const key of Object.keys(enteredFreq)) { + curFreq[key] = 0; + } + + return <> +
    +
    +
    -
    -
    -

    Extra Words

    -
      - { - this.state.extraWordsFound.map((word, i) => ( -
    • - {word.toUpperCase()} -
    • - )) +
      + { + this.state.letters.map((letter, k) => { + let letterClass = 'light-letter'; + const letterKey = letter.toLowerCase(); + if (letterKey in enteredFreq + && curFreq[letterKey] < enteredFreq[letterKey]) { + letterClass = 'dark-letter'; + curFreq[letterKey]++; } -
    -
    -
    -

    Words Found

    -
      {wordsFound}
    -
    -
    -

    Definitions

    - -
      {definitions}
    -
    + return ( + + {letter} + + ); + }) + }
    -
    -
    - -
    -
    - { - this.state.letters.map((letter, k) => { - let letterClass = 'light-letter'; - const letterKey = letter.toLowerCase(); - if (letterKey in enteredFreq - && curFreq[letterKey] < enteredFreq[letterKey]) { - letterClass = 'dark-letter'; - curFreq[letterKey]++; - } - return ( - - {letter} - - ); - }) - } -
    +
    +
    +
    +
    -
    -
    -
    -
    -
    -
    + + { this.setState({ shake: false }); }} + /> + -
    -
    + Enter + +
    + ; + } + + renderModeSelection = () => <> +

    Please select a mode:

    + + + ; + + render() { + if (!this.state.targetWordDefs) { + return (); + } + + return ( + +
    + { this.renderAnagramHeader() } + { + this.state.mode === UNSELECTED + ? this.renderModeSelection() + : <> + { this.renderHeader() } + { this.renderAnagramInfo() } + { this.renderInput() } + + } +