From f5af784ae6fb672326febf7a145e469bbc40068f Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Fri, 28 Oct 2022 16:06:24 -0400 Subject: [PATCH] improved answer checking and prompting --- package.json | 2 +- server/quizbowl.js | 56 ++++++++++++++++++++++++++++++------ tests/quizbowl.test.js | 64 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 952afc369..f205a6726 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qbreader", - "version": "2.6.1", + "version": "2.6.2", "scripts": { "start": "node server/server.js", "sass": "sass scss/light.scss client/bootstrap/light.css && sass scss/dark.scss client/bootstrap/dark.css" diff --git a/server/quizbowl.js b/server/quizbowl.js index 43fd21de5..6af4f7015 100644 --- a/server/quizbowl.js +++ b/server/quizbowl.js @@ -114,7 +114,14 @@ function parseAnswerline(answerline) { } -function stringMatchesReference(string, reference) { +/** + * + * @param {String} string + * @param {String} reference + * @param {Number} strictness - the number of characters per error allowed for two tokens to match. + * @returns {Boolean} + */ +function stringMatchesReference(string, reference, strictness=4) { if (string === null || string === undefined || reference === null || reference === undefined) { return false; } @@ -127,9 +134,19 @@ function stringMatchesReference(string, reference) { return string.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } + const stemmer = (string) => { + if (string.charAt(string.length - 1) === 's') { + return string.substring(0, string.length - 1); + } else { + return string; + } + } + string = removePunctuation(string); string = replaceSpecialCharacters(string); string = string.toLowerCase().trim(); + string = string.replace(/<\/?[biu]>/g, ''); + string = string.replace(/<\/?em>/g, ''); string = string.replace('-', ' '); reference = removePunctuation(reference); @@ -143,12 +160,22 @@ function stringMatchesReference(string, reference) { let stringTokens = string .split(' ') - .filter(token => !METAWORDS.includes(token) && token.length > 0) - .map(token => isNaN(token) ? token : toWords(parseInt(token))); + .filter(token => !METAWORDS.includes(token) && token.length > 0); + + for (let i = stringTokens.length - 1; i >= 0; i--) { + if (!isNaN(stringTokens[i])) { + stringTokens.push(toWords(parseInt(stringTokens[i]))); + } + } + let referenceTokens = reference .split(' ') - .filter(token => !METAWORDS.includes(token) && token.length > 0) - .map(token => isNaN(token) ? token : toWords(parseInt(token))); + .filter(token => !METAWORDS.includes(token) && token.length > 0); + for (let i = referenceTokens.length - 1; i >= 0; i--) { + if (!isNaN(referenceTokens[i])) { + referenceTokens.push(toWords(parseInt(referenceTokens[i]))); + } + } if (stringTokens.length === 0) { return false; @@ -163,11 +190,14 @@ function stringMatchesReference(string, reference) { let tokenMatches = false; for (let j = 0; j < referenceTokens.length; j++) { - let errors = dljs.distance(stringTokens[i], referenceTokens[j]); + let errors = dljs.distance(stemmer(stringTokens[i]), stemmer(referenceTokens[j])); - if (4 * errors <= referenceTokens[j].length) { + // console.log(stringTokens[i], referenceTokens[j]); + if (strictness * errors <= referenceTokens[j].length || referenceTokens[j].includes(stringTokens[i])) { tokenMatches = true; break; + } else { + // console.log(errors, stringTokens[j], referenceTokens[j]); } } @@ -214,7 +244,7 @@ function checkAnswer(answerline, givenAnswer) { const parsedAnswerline = parseAnswerline(answerline); for (const answer of parsedAnswerline['reject']) { - if (stringMatchesReference(answer[2], givenAnswer) && stringMatchesReference(givenAnswer, answer[2])) { + if (stringMatchesReference(answer[2], givenAnswer, 7) && stringMatchesReference(givenAnswer, answer[2], 7)) { return 'reject'; } } @@ -237,6 +267,16 @@ function checkAnswer(answerline, givenAnswer) { } } + if (answerline.includes('[prompt on partial') || answerline.includes('(prompt on partial')) { + const [answer1, answer2] = parsedAnswerline.accept[0][0].split(' '); + if (answerWorks(answer1, givenAnswer, isFormattedAnswerline)) { + return 'prompt'; + } + if (answerWorks(answer2, givenAnswer, isFormattedAnswerline)) { + return 'prompt'; + } + } + return 'reject'; } diff --git a/tests/quizbowl.test.js b/tests/quizbowl.test.js index 8f1635f47..955c4ed6c 100644 --- a/tests/quizbowl.test.js +++ b/tests/quizbowl.test.js @@ -12,13 +12,24 @@ const formatted_answers = [ "Louis-Philippe [or Duke d’Orleans; prompt on “Citizen King” before mentioned]", "Johann Tserclaes, Graf von Tilly (accept either underlined answer as well as Count of Tilly)", "Paul Bäumer [accept either name]", - "Matsuo Bashō [accept either underlined part; accept Matsuo Kinsaku or Matsuo Chūemon Munefusa]" + "Matsuo Bashō [accept either underlined part; accept Matsuo Kinsaku or Matsuo Chūemon Munefusa]", + "Prime Minister of Australia [prompt on partial answers]", + "hypothesis test", + "graphene [do not accept or prompt on \"graphite\"]", + "amides [do not accept or prompt on \"amines\"]", + "cosmic microwave background radiation [or CMB; or CMBR]", + "1980s [prompt on 80s]", + "working memory [prompt on partial answers or on “short-term memory”]", + "Pyotr Ilyich Tchaikovsky’s Piano Concerto No. 1 [accept Tchaikovsky’s PianoConcerto in B-flat minor until “B-flat” is read; accept word forms like Tchaikovsky’s first piano concerto; prompt on partial answer]", ]; const answers = [ "Heinrich Böll [or Heinrich Theodor Böll]", "primatology [or word forms; accept any answers about the study of great apes, nonhuman primates, gorillas, bonobos, or chimpanzees; prompt on the study of monkeys or simians; prompt on word forms of ethology, biology, anthropology, or evolutionary or social psychology; prompt on the study of animals with “what type of animals?”]", - "China [or People’s Republic of China; do not accept or prompt on “Republic of China”]" + "China [or People’s Republic of China; do not accept or prompt on “Republic of China”]", + "amides [do not accept or prompt on \"amines\"]", + "RAF [or Red Army Faction; accept Baader–Meinhof group; accept Baader–Meinhof gang; accept Rote Armee Fraktion] (The Action Directe communiqué was also signed “kommando elisabeth van dyck,” in reference to a fallen member of RAF.)", + "Lenski's longterm E. coli evolution experiment [accept anything mentioning the long term evolution of E. Coli]", ]; const tests = [ @@ -79,19 +90,66 @@ const tests = [ ['accept', formatted_answers[11], 'Matsuo Basho'], ['accept', formatted_answers[11], 'Matsuo Bashō'], + ['accept', formatted_answers[12], 'prime minister of australia'], + ['accept', formatted_answers[12], 'australia prime minister'], + ['accept', formatted_answers[12], 'australian prime minister'], + ['prompt', formatted_answers[12], 'prime minister'], + + ['accept', formatted_answers[13], 'hypothesis testing'], + ['accept', formatted_answers[13], 'testing'], + ['accept', formatted_answers[13], 'test'], + + ['accept', formatted_answers[14], 'graphene'], + ['reject', formatted_answers[14], 'graphite'], + + ['accept', formatted_answers[15], 'amides'], + ['accept', formatted_answers[15], 'amide'], + ['reject', formatted_answers[15], 'amine'], + + ['accept', formatted_answers[16], 'cosmic microwave background radiation'], + ['accept', formatted_answers[16], 'cosmic microwave background'], + ['accept', formatted_answers[16], 'cmb'], + ['accept', formatted_answers[16], 'cmbr'], + + ['accept', formatted_answers[17], '1980s'], + ['accept', formatted_answers[17], '1980'], + ['prompt', formatted_answers[17], '80'], + ['prompt', formatted_answers[17], '80s'], + ['reject', formatted_answers[17], '90s'], + ['reject', formatted_answers[17], '90'], + // ['reject', formatted_answers[17], '1990'], // TODO + // ['reject', formatted_answers[17], '1990s'], // TODO + + ['accept', formatted_answers[18], 'working memory'], + ['prompt', formatted_answers[18], 'memory'], + + ['accept', formatted_answers[19], 'Tchaikovsky Piano Concerto no 1'], + // ['prompt', formatted_answers[19], 'Piano Concerto'], // TODO + ['accept', answers[0], 'boll'], ['accept', answers[0], 'heinrich boll'], ['accept', answers[0], 'Böll'], ['accept', answers[0], 'Heinrich Böll'], // unformatted answerlines - ['reject', answers[1], 'chimp'], // TODO: make this accept + ['accept', answers[1], 'chimp'], ['accept', answers[1], 'chimpanzee'], // reject clauses that are a subset of acceptable answer ['accept', answers[2], 'China'], ['accept', answers[2], 'people’s republic of China'], ['reject', answers[2], 'republic of china'], + + ['accept', answers[3], 'amides'], + ['accept', answers[3], 'amide'], + ['reject', answers[3], 'amine'], + + ['accept', answers[4], 'baader meinhof'], + ['accept', answers[4], 'raf'], + ['accept', answers[4], 'red army faction'], + ['accept', answers[4], 'red army'], + + ['accept', answers[5], 'lenski long term e coli experiment'], ]; let successful = 0, total = 0;