more words
This commit is contained in:
@@ -1,22 +1,9 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import wordlistEnglish from 'wordlist-english';
|
import { buildWordSet } from './lexicon-sources.mjs';
|
||||||
|
|
||||||
const validTwo = new Set([
|
|
||||||
'am', 'an', 'as', 'at', 'be', 'by', 'do', 'go', 'he', 'hi',
|
|
||||||
'if', 'in', 'is', 'it', 'me', 'my', 'no', 'of', 'oh', 'on',
|
|
||||||
'or', 'so', 'to', 'up', 'us', 'we'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Build a clean, high-frequency recognized vocabulary for client-side unlimited/custom puzzles
|
// Build a clean, high-frequency recognized vocabulary for client-side unlimited/custom puzzles
|
||||||
const vocabulary = new Set();
|
// (includes regional spellings and the proper-word allowlist)
|
||||||
for (const word of wordlistEnglish.english) {
|
const vocabulary = buildWordSet();
|
||||||
const w = word.toLowerCase().trim();
|
|
||||||
if (w.length === 2 && validTwo.has(w)) {
|
|
||||||
vocabulary.add(w);
|
|
||||||
} else if (w.length >= 3 && w.length <= 16 && /^[a-z]+$/.test(w)) {
|
|
||||||
vocabulary.add(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Total vocabulary words:', vocabulary.size);
|
console.log('Total vocabulary words:', vocabulary.size);
|
||||||
|
|
||||||
|
|||||||
@@ -466,6 +466,249 @@ slippers|sl-ip-pe-rs|home|Soft shoes worn indoors
|
|||||||
potatoes|po-ta-to-es|food|Starchy vegetables used for fries
|
potatoes|po-ta-to-es|food|Starchy vegetables used for fries
|
||||||
sausages|sa-us-ag-es|food|Seasoned meat in a casing
|
sausages|sa-us-ag-es|food|Seasoned meat in a casing
|
||||||
pretzels|pr-et-ze-ls|food|Twisted salty baked snacks
|
pretzels|pr-et-ze-ls|food|Twisted salty baked snacks
|
||||||
|
absolute|ab-so-lu-te|everyday|Complete and total
|
||||||
|
academic|ac-ad-em-ic|education|Relating to school or scholarship
|
||||||
|
accident|ac-ci-de-nt|everyday|Unplanned event that causes harm
|
||||||
|
accidental|ac-ci-den-tal|everyday|Happening by chance
|
||||||
|
accuracy|ac-cu-ra-cy|everyday|Quality of being correct and precise
|
||||||
|
accurate|ac-cu-ra-te|everyday|Correct in every detail
|
||||||
|
activity|ac-ti-vi-ty|everyday|Something you do for fun or a purpose
|
||||||
|
additional|ad-di-tion-al|everyday|Added or extra
|
||||||
|
advertise|ad-ver-ti-se|business|Promote a product publicly
|
||||||
|
agreement|ag-re-em-ent|business|Shared understanding or deal
|
||||||
|
alternative|al-ter-na-tive|everyday|Another possible choice
|
||||||
|
analysis|an-al-ys-is|science|Detailed examination of something
|
||||||
|
announce|an-no-un-ce|everyday|Make known publicly
|
||||||
|
anonymous|ano-ny-mo-us|everyday|Of unknown name
|
||||||
|
appearance|ap-pea-ra-nce|everyday|The way someone or something looks
|
||||||
|
appreciate|ap-pre-ci-ate|emotion|Recognize the value of
|
||||||
|
approach|ap-pr-oa-ch|everyday|Come near to
|
||||||
|
appropriate|ap-pro-pri-ate|everyday|Suitable for the situation
|
||||||
|
argument|ar-gu-me-nt|everyday|Heated disagreement
|
||||||
|
arrangement|ar-ran-ge-ment|everyday|Plan or organized layout
|
||||||
|
assembly|as-se-mb-ly|education|Gathering of people, as at school
|
||||||
|
assistant|as-si-st-ant|jobs|Person who helps another
|
||||||
|
associate|as-so-ci-ate|business|Connect in the mind; a colleague
|
||||||
|
attitude|at-ti-tu-de|emotion|Way of thinking or feeling
|
||||||
|
attractive|at-tra-cti-ve|everyday|Pleasing to look at
|
||||||
|
audience|au-di-en-ce|arts|People watching a performance
|
||||||
|
authority|au-th-or-ity|society|Power to give orders
|
||||||
|
automatic|au-to-ma-tic|tech|Working by itself
|
||||||
|
available|av-ai-la-ble|everyday|Ready to be used
|
||||||
|
background|ba-ck-gro-und|arts|Scenery behind the main subject
|
||||||
|
broadcast|bro-ad-ca-st|tech|Transmit on radio or TV
|
||||||
|
bulletin|bu-ll-et-in|everyday|Short official news update
|
||||||
|
business|bu-si-ne-ss|business|Company or commercial activity
|
||||||
|
calculate|ca-lc-ul-ate|education|Work out with math
|
||||||
|
candidate|can-di-da-te|society|Person running for office
|
||||||
|
capacity|ca-pa-ci-ty|everyday|Maximum amount something can hold
|
||||||
|
category|ca-te-go-ry|everyday|Group of similar things
|
||||||
|
character|ch-ar-ac-ter|arts|Person in a story
|
||||||
|
chemical|ch-em-ic-al|science|Substance made by chemistry
|
||||||
|
collapse|co-ll-ap-se|everyday|Fall down suddenly
|
||||||
|
commercial|com-mer-ci-al|business|TV advertisement
|
||||||
|
committee|com-mi-tt-ee|society|Group chosen to make decisions
|
||||||
|
communicate|com-mu-nic-ate|everyday|Share information
|
||||||
|
community|com-mu-ni-ty|society|People living in one area
|
||||||
|
comparison|com-pa-ri-son|everyday|Looking at similarities and differences
|
||||||
|
complain|co-mp-la-in|emotion|Express unhappiness
|
||||||
|
complaint|co-mp-la-int|everyday|Statement of dissatisfaction
|
||||||
|
complete|co-mp-le-te|everyday|Finished; whole
|
||||||
|
compromise|com-pro-mi-se|society|Deal where both sides give a little
|
||||||
|
conclusion|co-nc-lu-sion|education|The end or final decision
|
||||||
|
concrete|co-nc-re-te|city|Hard building material for sidewalks
|
||||||
|
conference|co-nf-ere-nce|business|Formal meeting for discussion
|
||||||
|
confident|con-fi-de-nt|emotion|Sure of yourself
|
||||||
|
consistent|co-ns-ist-ent|everyday|Unchanging over time
|
||||||
|
constant|co-ns-ta-nt|everyday|Never stopping
|
||||||
|
continue|co-nt-in-ue|everyday|Keep going
|
||||||
|
continuous|con-ti-nu-ous|everyday|Without a break
|
||||||
|
contract|co-nt-ra-ct|business|Written legal agreement
|
||||||
|
contrast|co-nt-ra-st|arts|Striking difference
|
||||||
|
contribute|con-tri-bu-te|society|Give to a shared cause
|
||||||
|
convenient|con-ve-ni-ent|everyday|Easy and handy
|
||||||
|
creature|cr-ea-tu-re|animals|Living animal or being
|
||||||
|
critical|cr-it-ic-al|everyday|Extremely important; faultfinding
|
||||||
|
dangerous|da-ng-er-ous|everyday|Likely to cause harm
|
||||||
|
decision|de-ci-si-on|everyday|A choice that has been made
|
||||||
|
decrease|de-cr-ea-se|everyday|Make smaller
|
||||||
|
dedicate|de-di-ca-te|emotion|Devote to a purpose
|
||||||
|
definite|de-fi-ni-te|everyday|Clearly decided
|
||||||
|
deliberate|de-li-ber-ate|everyday|Done on purpose
|
||||||
|
delivery|de-li-ve-ry|everyday|Bringing goods to the door
|
||||||
|
democratic|de-mo-cra-tic|history|Ruled by the people
|
||||||
|
demonstrate|de-mon-stra-te|education|Show how something works
|
||||||
|
department|de-pa-rt-ment|places|Division of a store or company
|
||||||
|
describe|de-sc-ri-be|words|Tell what something is like
|
||||||
|
desperate|de-sp-er-ate|emotion|Willing to try anything
|
||||||
|
determine|de-ter-mi-ne|everyday|Figure out or decide
|
||||||
|
difference|di-ff-ere-nce|everyday|Way in which things are unlike
|
||||||
|
different|di-ff-er-ent|everyday|Not the same
|
||||||
|
director|di-re-ct-or|arts|Person in charge of making a film
|
||||||
|
disagree|di-sa-gr-ee|everyday|Have a different opinion
|
||||||
|
disappear|di-sa-pp-ear|fantasy|Vanish from sight
|
||||||
|
disaster|di-sa-st-er|nature|Sudden terrible event
|
||||||
|
discount|di-sc-ou-nt|business|Reduction in price
|
||||||
|
discussion|di-sc-us-sion|everyday|Conversation about a topic
|
||||||
|
distance|di-st-an-ce|travel|How far apart things are
|
||||||
|
distinct|di-st-in-ct|everyday|Clearly different
|
||||||
|
district|di-st-ri-ct|city|Area of a city or country
|
||||||
|
division|di-vi-si-on|education|Splitting a number into parts
|
||||||
|
document|do-cu-me-nt|business|Official paper or file
|
||||||
|
economic|ec-on-om-ic|business|Relating to money and trade
|
||||||
|
effective|ef-fe-cti-ve|everyday|Producing the desired result
|
||||||
|
efficient|ef-fi-ci-ent|everyday|Working without waste
|
||||||
|
electric|el-ec-tr-ic|tech|Powered by electricity
|
||||||
|
embarrass|em-ba-rr-ass|emotion|Make someone feel awkward
|
||||||
|
emergency|em-er-gen-cy|health|Serious situation needing quick action
|
||||||
|
emphasis|em-ph-as-is|words|Special importance
|
||||||
|
encounter|en-co-un-ter|everyday|Unexpected meeting
|
||||||
|
encourage|en-cou-ra-ge|emotion|Give support and confidence
|
||||||
|
enormous|en-or-mo-us|everyday|Very large
|
||||||
|
entrance|en-tr-an-ce|places|Way in
|
||||||
|
equipment|equ-ip-me-nt|sports|Tools needed for a job or sport
|
||||||
|
essential|ess-en-ti-al|everyday|Absolutely necessary
|
||||||
|
estimate|es-ti-ma-te|education|Rough calculation
|
||||||
|
excellent|ex-ce-ll-ent|emotion|Extremely good
|
||||||
|
exchange|ex-ch-an-ge|business|Trade one thing for another
|
||||||
|
existence|ex-is-te-nce|mind|State of being real
|
||||||
|
expensive|ex-pen-si-ve|business|Costing a lot
|
||||||
|
expression|ex-pr-es-sion|emotion|Look on someone's face
|
||||||
|
facility|fa-ci-li-ty|places|Building made for a purpose
|
||||||
|
familiar|fa-mi-li-ar|everyday|Well known
|
||||||
|
feedback|fe-ed-ba-ck|business|Comments on how you did
|
||||||
|
financial|fin-an-ci-al|business|Relating to money
|
||||||
|
forthcoming|for-th-com-ing|everyday|About to happen
|
||||||
|
frequent|fr-eq-ue-nt|everyday|Happening often
|
||||||
|
generate|ge-ne-ra-te|tech|Produce or create
|
||||||
|
governor|go-ve-rn-or|history|Elected head of a state
|
||||||
|
graduate|gr-ad-ua-te|education|Finish school
|
||||||
|
grateful|gr-at-ef-ul|emotion|Thankful
|
||||||
|
guarantee|gu-ar-ant-ee|business|Firm promise
|
||||||
|
harmless|ha-rm-le-ss|everyday|Not able to hurt
|
||||||
|
historical|hi-sto-ri-cal|history|Relating to the past
|
||||||
|
immediate|im-me-di-ate|everyday|Happening right away
|
||||||
|
importance|im-por-ta-nce|everyday|State of mattering a lot
|
||||||
|
important|im-po-rt-ant|everyday|Of great value or significance
|
||||||
|
impression|im-pr-es-sion|emotion|Idea formed about someone
|
||||||
|
improvement|im-pro-ve-ment|everyday|Change for the better
|
||||||
|
incident|in-ci-de-nt|everyday|Event or occurrence
|
||||||
|
incomplete|in-com-ple-te|everyday|Not finished
|
||||||
|
increase|in-cr-ea-se|everyday|Make larger
|
||||||
|
indicate|in-di-ca-te|everyday|Point out
|
||||||
|
infinite|in-fi-ni-te|space|Without end
|
||||||
|
influence|in-fl-ue-nce|society|Power to affect others
|
||||||
|
innocent|in-no-ce-nt|emotion|Not guilty
|
||||||
|
instance|in-st-an-ce|everyday|An example
|
||||||
|
insurance|in-su-ra-nce|business|Protection against loss
|
||||||
|
intelligent|in-tel-lig-ent|mind|Smart
|
||||||
|
interact|in-te-ra-ct|society|Act with one another
|
||||||
|
irritate|ir-ri-ta-te|emotion|Annoy
|
||||||
|
language|la-ng-ua-ge|words|System of words people speak
|
||||||
|
lifetime|li-fe-ti-me|everyday|Length of a life
|
||||||
|
majority|ma-jo-ri-ty|society|More than half
|
||||||
|
material|ma-te-ri-al|craft|Fabric or substance things are made of
|
||||||
|
meaningful|me-an-ing-ful|emotion|Full of significance
|
||||||
|
membership|me-mb-ers-hip|society|Belonging to a group
|
||||||
|
military|mi-li-ta-ry|history|Armed forces
|
||||||
|
minority|mi-no-ri-ty|society|Less than half
|
||||||
|
movement|mo-ve-me-nt|everyday|Act of moving
|
||||||
|
national|na-ti-on-al|society|Relating to a nation
|
||||||
|
necessary|nec-es-sa-ry|everyday|Required
|
||||||
|
necessity|ne-ce-ss-ity|everyday|Something you need
|
||||||
|
negative|ne-ga-ti-ve|everyday|Opposite of positive
|
||||||
|
nonsense|no-ns-en-se|words|Silly talk
|
||||||
|
occasion|oc-ca-si-on|everyday|Special event
|
||||||
|
official|of-fi-ci-al|society|Formally approved
|
||||||
|
opposite|op-po-si-te|words|Completely different
|
||||||
|
optional|op-ti-on-al|everyday|Not required
|
||||||
|
ordinary|or-di-na-ry|everyday|Normal; everyday
|
||||||
|
original|or-ig-in-al|arts|The first version
|
||||||
|
paragraph|pa-ra-gra-ph|words|Block of sentences
|
||||||
|
parallel|pa-ra-ll-el|education|Side by side, never meeting
|
||||||
|
particular|par-ti-cu-lar|everyday|Specific; fussy
|
||||||
|
peculiar|pe-cu-li-ar|everyday|Strange or odd
|
||||||
|
performance|per-for-ma-nce|arts|Show in front of an audience
|
||||||
|
permanent|pe-rm-an-ent|everyday|Lasting forever
|
||||||
|
permission|per-mis-si-on|everyday|Being allowed to do something
|
||||||
|
personal|pe-rs-on-al|everyday|Private; your own
|
||||||
|
persuade|pe-rs-ua-de|words|Convince
|
||||||
|
physical|ph-ys-ic-al|health|Relating to the body
|
||||||
|
pleasant|pl-ea-sa-nt|emotion|Nice and enjoyable
|
||||||
|
pointless|po-in-tl-ess|everyday|Having no purpose
|
||||||
|
political|po-li-ti-cal|society|Relating to government
|
||||||
|
positive|po-si-ti-ve|emotion|Hopeful; greater than zero
|
||||||
|
potential|pot-en-ti-al|everyday|Possible future ability
|
||||||
|
powerful|po-we-rf-ul|everyday|Very strong
|
||||||
|
practical|pr-ac-ti-cal|everyday|Useful in real life
|
||||||
|
practice|pr-ac-ti-ce|sports|Repeat to improve
|
||||||
|
preference|pr-ef-ere-nce|everyday|Something liked more
|
||||||
|
presence|pr-es-en-ce|everyday|Being there
|
||||||
|
preserve|pr-es-er-ve|food|Keep safe; fruit jam
|
||||||
|
pressure|pr-es-su-re|science|Force pushing on something
|
||||||
|
previous|pr-ev-io-us|everyday|Coming before
|
||||||
|
principle|pr-in-ci-ple|mind|Basic rule or belief
|
||||||
|
procedure|pr-oc-ed-ure|health|Set way of doing something
|
||||||
|
progress|pr-og-re-ss|everyday|Forward movement
|
||||||
|
property|pr-op-er-ty|home|Land or belongings
|
||||||
|
proposal|pr-op-os-al|emotion|Plan or offer, such as of marriage
|
||||||
|
prospect|pr-os-pe-ct|business|Possibility of future success
|
||||||
|
puncture|pu-nc-tu-re|travel|Small hole, like in a tire
|
||||||
|
quantity|qu-an-ti-ty|everyday|Amount
|
||||||
|
recommend|re-com-me-nd|everyday|Suggest as good
|
||||||
|
register|re-gi-st-er|business|Sign up officially
|
||||||
|
relationship|re-la-tion-ship|emotion|Connection between people
|
||||||
|
relative|re-la-ti-ve|everyday|Family member
|
||||||
|
relevant|re-le-va-nt|everyday|Related to the topic
|
||||||
|
reliable|re-li-ab-le|everyday|Dependable
|
||||||
|
religion|re-li-gi-on|society|Belief system of worship
|
||||||
|
religious|re-li-gi-ous|society|Devoted to a faith
|
||||||
|
reluctant|re-lu-ct-ant|emotion|Unwilling
|
||||||
|
remember|re-me-mb-er|mind|Recall from memory
|
||||||
|
replacement|re-pl-ace-ment|everyday|Thing used instead
|
||||||
|
represent|re-pr-es-ent|society|Stand for or speak for
|
||||||
|
research|re-se-ar-ch|science|Careful study to learn facts
|
||||||
|
resident|re-si-de-nt|home|Person who lives somewhere
|
||||||
|
resource|re-so-ur-ce|everyday|Useful supply
|
||||||
|
response|re-sp-on-se|words|Answer or reply
|
||||||
|
restrict|re-st-ri-ct|everyday|Limit
|
||||||
|
ridiculous|ri-dic-ul-ous|emotion|Absurd
|
||||||
|
secretary|se-cre-ta-ry|jobs|Office assistant
|
||||||
|
security|se-cu-ri-ty|tech|Safety and protection
|
||||||
|
sentence|se-nt-en-ce|words|Group of words expressing a complete thought
|
||||||
|
separate|se-pa-ra-te|everyday|Set apart
|
||||||
|
sequence|se-qu-en-ce|everyday|Order in which things follow
|
||||||
|
shortage|sh-or-ta-ge|business|Not enough of something
|
||||||
|
something|so-me-th-ing|everyday|An unspecified thing
|
||||||
|
southern|so-ut-he-rn|travel|In or from the south
|
||||||
|
standard|st-an-da-rd|everyday|Usual level of quality
|
||||||
|
statement|st-at-em-ent|words|Something said or written formally
|
||||||
|
straight|st-ra-ig-ht|everyday|Without a bend
|
||||||
|
strategy|st-ra-te-gy|games|Plan to reach a goal
|
||||||
|
strength|st-re-ng-th|sports|Being strong
|
||||||
|
substitute|sub-sti-tu-te|sports|Replacement player
|
||||||
|
successful|su-cc-ess-ful|endeavor|Achieving a goal
|
||||||
|
suitable|su-it-ab-le|everyday|Right for the purpose
|
||||||
|
superior|su-pe-ri-or|everyday|Better; higher in rank
|
||||||
|
surprise|su-rp-ri-se|emotion|Unexpected event
|
||||||
|
suspicion|su-spi-ci-on|mystery|Feeling that something is wrong
|
||||||
|
technical|te-ch-ni-cal|tech|Relating to technology or skill
|
||||||
|
technique|te-ch-ni-que|arts|Way of doing something skillfully
|
||||||
|
temporary|te-mp-ora-ry|everyday|Lasting a short time
|
||||||
|
tendency|te-nd-en-cy|mind|Habit or inclination
|
||||||
|
thousand|th-ou-sa-nd|education|Ten hundred
|
||||||
|
together|to-ge-th-er|society|With each other
|
||||||
|
traditional|tra-di-tion-al|history|Following long-held customs
|
||||||
|
translate|tr-an-sl-ate|words|Change into another language
|
||||||
|
transport|tr-an-spo-rt|travel|Carry from place to place
|
||||||
|
treatment|tre-at-me-nt|health|Medical care
|
||||||
|
ultimate|ul-ti-ma-te|everyday|Final; best possible
|
||||||
|
understand|un-der-sta-nd|mind|Grasp the meaning
|
||||||
|
universal|un-iv-ers-al|everyday|Applying to everyone
|
||||||
|
unpleasant|un-ple-as-ant|emotion|Not nice
|
||||||
|
valuable|va-lu-ab-le|everyday|Worth a lot
|
||||||
|
widespread|wi-de-spr-ead|everyday|Found over a large area
|
||||||
|
worthwhile|wor-th-whi-le|everyday|Worth the time or effort
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const extraWords = RAW.trim()
|
export const extraWords = RAW.trim()
|
||||||
|
|||||||
90
scripts/find-chunk-words.mjs
Normal file
90
scripts/find-chunk-words.mjs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Finds common English words that can be built from exactly 4 existing tiles
|
||||||
|
// (chunks) in the target-word pool, but aren't target words yet.
|
||||||
|
//
|
||||||
|
// Usage: node scripts/find-chunk-words.mjs [--max-tier=35] [--json]
|
||||||
|
import wl from 'wordlist-english';
|
||||||
|
import { DICTIONARY_SET } from '../src/data/lexicon.js';
|
||||||
|
import { TARGET_WORDS_POOL } from '../src/data/puzzles.js';
|
||||||
|
|
||||||
|
const args = Object.fromEntries(process.argv.slice(2).map(a => a.replace(/^--/, '').split('=')));
|
||||||
|
const MAX_TIER = Number(args['max-tier'] || 35);
|
||||||
|
const TIERS = [10, 20, 35, 40, 50].filter(t => t <= MAX_TIER);
|
||||||
|
|
||||||
|
// Every chunk currently used by a target word
|
||||||
|
const chunks = new Set(TARGET_WORDS_POOL.flatMap(w => w.tiles.map(t => t.toLowerCase())));
|
||||||
|
const existing = new Set(TARGET_WORDS_POOL.map(w => w.word));
|
||||||
|
|
||||||
|
// Common words only (frequency tiers from wordlist-english)
|
||||||
|
const candidates = new Set();
|
||||||
|
for (const t of TIERS) for (const w of wl[`english/${t}`] || []) candidates.add(w);
|
||||||
|
|
||||||
|
// Try to split a word into exactly 4 distinct chunks (each >= 2 letters).
|
||||||
|
// Returns the most balanced split, or null.
|
||||||
|
function split(word) {
|
||||||
|
let best = null;
|
||||||
|
const n = word.length;
|
||||||
|
for (let a = 2; a <= n - 6; a++) {
|
||||||
|
const t1 = word.slice(0, a);
|
||||||
|
if (!chunks.has(t1)) continue;
|
||||||
|
for (let b = a + 2; b <= n - 4; b++) {
|
||||||
|
const t2 = word.slice(a, b);
|
||||||
|
if (!chunks.has(t2)) continue;
|
||||||
|
for (let c = b + 2; c <= n - 2; c++) {
|
||||||
|
const t3 = word.slice(b, c);
|
||||||
|
const t4 = word.slice(c);
|
||||||
|
if (!chunks.has(t3) || !chunks.has(t4)) continue;
|
||||||
|
const tiles = [t1, t2, t3, t4];
|
||||||
|
if (new Set(tiles).size !== 4) continue;
|
||||||
|
const lens = tiles.map(t => t.length);
|
||||||
|
const spread = Math.max(...lens) - Math.min(...lens);
|
||||||
|
if (!best || spread < best.spread) best = { tiles, spread };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best && best.tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip inflected forms whose base word exists (walked, happier, quickly, kittens...)
|
||||||
|
const allWords = new Set(wl.english);
|
||||||
|
function isInflection(w) {
|
||||||
|
const bases = [];
|
||||||
|
if (w.endsWith('ies')) bases.push(w.slice(0, -3) + 'y');
|
||||||
|
if (w.endsWith('es')) bases.push(w.slice(0, -2));
|
||||||
|
if (w.endsWith('s')) bases.push(w.slice(0, -1));
|
||||||
|
if (w.endsWith('ed')) bases.push(w.slice(0, -2), w.slice(0, -1), w.slice(0, -3));
|
||||||
|
if (w.endsWith('ied')) bases.push(w.slice(0, -3) + 'y');
|
||||||
|
if (w.endsWith('ing')) bases.push(w.slice(0, -3), w.slice(0, -3) + 'e', w.slice(0, -4));
|
||||||
|
if (w.endsWith('ly')) bases.push(w.slice(0, -2), w.slice(0, -3) + 'e');
|
||||||
|
if (w.endsWith('ily')) bases.push(w.slice(0, -3) + 'y');
|
||||||
|
if (w.endsWith('er')) bases.push(w.slice(0, -2), w.slice(0, -1), w.slice(0, -3));
|
||||||
|
if (w.endsWith('est')) bases.push(w.slice(0, -3), w.slice(0, -2), w.slice(0, -4));
|
||||||
|
if (w.endsWith('ness')) bases.push(w.slice(0, -4));
|
||||||
|
return bases.some(b => b.length >= 3 && allWords.has(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip near-duplicates of words already in the pool (e.g. "dolphin" vs "dolphins")
|
||||||
|
function isInflectionOfExisting(w) {
|
||||||
|
const stems = [w.replace(/s$/, ''), w.replace(/es$/, ''), w.replace(/ies$/, 'y'), w + 's', w + 'es', w.replace(/y$/, 'ies')];
|
||||||
|
return stems.some(s => s !== w && existing.has(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
const found = [];
|
||||||
|
for (const word of candidates) {
|
||||||
|
if (word.length < 8 || word.length > 14) continue;
|
||||||
|
if (!/^[a-z]+$/.test(word)) continue;
|
||||||
|
if (existing.has(word)) continue;
|
||||||
|
if (word.endsWith('tion')) continue;
|
||||||
|
if (!DICTIONARY_SET.has(word)) continue;
|
||||||
|
if (isInflectionOfExisting(word)) continue;
|
||||||
|
if (!('keep-inflections' in args) && isInflection(word)) continue;
|
||||||
|
const tiles = split(word);
|
||||||
|
if (tiles) found.push({ word, tiles });
|
||||||
|
}
|
||||||
|
|
||||||
|
found.sort((a, b) => a.word.localeCompare(b.word));
|
||||||
|
if ('json' in args) {
|
||||||
|
console.log(JSON.stringify(found));
|
||||||
|
} else {
|
||||||
|
for (const f of found) console.log(`${f.word}|${f.tiles.join('-')}`);
|
||||||
|
console.error(`\n${chunks.size} chunks, ${found.length} new words found (tiers <= ${MAX_TIER})`);
|
||||||
|
}
|
||||||
68
scripts/lexicon-sources.mjs
Normal file
68
scripts/lexicon-sources.mjs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import wordlistEnglish from 'wordlist-english';
|
||||||
|
|
||||||
|
// Shared dictionary sources for the client lexicon and the build-time solver.
|
||||||
|
|
||||||
|
// Valid 2-letter English words accepted in standard games
|
||||||
|
export const VALID_TWO = new Set([
|
||||||
|
'am', 'an', 'as', 'at', 'be', 'by', 'do', 'go', 'he', 'hi',
|
||||||
|
'if', 'in', 'is', 'it', 'me', 'my', 'no', 'of', 'oh', 'on',
|
||||||
|
'or', 'so', 'to', 'up', 'us', 'we'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// The base `english` list excludes regional spellings (neighborhood, color,
|
||||||
|
// theater...), so merge in every regional variant list.
|
||||||
|
const SOURCE_LISTS = [
|
||||||
|
'english',
|
||||||
|
'english/american',
|
||||||
|
'english/british',
|
||||||
|
'english/canadian',
|
||||||
|
'english/australian'
|
||||||
|
];
|
||||||
|
|
||||||
|
// wordlist-english has no proper nouns. Commonly-used proper adjectives and
|
||||||
|
// names that players reasonably expect to be accepted go here.
|
||||||
|
export const PROPER_WORD_ALLOWLIST = [
|
||||||
|
'mediterranean', 'atlantic', 'pacific', 'caribbean', 'arctic', 'antarctic',
|
||||||
|
'european', 'african', 'asian', 'american', 'canadian', 'mexican',
|
||||||
|
'australian', 'arabian', 'egyptian', 'roman', 'greek', 'latin', 'italian',
|
||||||
|
'spanish', 'french', 'english', 'german', 'chinese', 'japanese', 'indian',
|
||||||
|
'olympic', 'olympics', 'christmas', 'easter', 'halloween', 'thanksgiving',
|
||||||
|
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
||||||
|
'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august',
|
||||||
|
'september', 'october', 'november', 'december',
|
||||||
|
'mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune',
|
||||||
|
'amazon', 'sahara', 'everest', 'antarctica', 'africa', 'europe', 'america',
|
||||||
|
'asia', 'australia'
|
||||||
|
];
|
||||||
|
|
||||||
|
function isAcceptable(w) {
|
||||||
|
if (w.length === 2) return VALID_TWO.has(w);
|
||||||
|
return w.length >= 3 && w.length <= 16 && /^[a-z]+$/.test(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWordSet() {
|
||||||
|
const set = new Set();
|
||||||
|
for (const key of SOURCE_LISTS) {
|
||||||
|
for (const raw of wordlistEnglish[key] || []) {
|
||||||
|
const w = raw.toLowerCase().trim();
|
||||||
|
if (isAcceptable(w)) set.add(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const w of PROPER_WORD_ALLOWLIST) {
|
||||||
|
if (isAcceptable(w)) set.add(w);
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commonness tiers (base + American lists) for word quality ranking
|
||||||
|
const tiers = [10, 20, 35, 50].map(n => new Set([
|
||||||
|
...(wordlistEnglish[`english/${n}`] || []),
|
||||||
|
...(wordlistEnglish[`english/american/${n}`] || [])
|
||||||
|
]));
|
||||||
|
|
||||||
|
export function getCommonnessScore(word) {
|
||||||
|
for (let i = 0; i < tiers.length; i++) {
|
||||||
|
if (tiers[i].has(word)) return 4 - i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,37 +1,9 @@
|
|||||||
import fs from 'node:fs';
|
import { buildWordSet, getCommonnessScore } from './lexicon-sources.mjs';
|
||||||
import wordlistEnglish from 'wordlist-english';
|
import { capWordList } from '../src/engine/word-cap.js';
|
||||||
|
|
||||||
// Valid 2-letter English words accepted in standard games
|
// Clean dictionary incl. regional spellings + proper-word allowlist
|
||||||
const validTwo = new Set([
|
// (same source as src/data/lexicon.js)
|
||||||
'am', 'an', 'as', 'at', 'be', 'by', 'do', 'go', 'he', 'hi',
|
const cleanWordSet = buildWordSet();
|
||||||
'if', 'in', 'is', 'it', 'me', 'my', 'no', 'of', 'oh', 'on',
|
|
||||||
'or', 'so', 'to', 'up', 'us', 'we'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Build standard clean dictionary (excluding vulgarities, numbers, non-alpha)
|
|
||||||
const cleanWordSet = new Set();
|
|
||||||
for (const raw of wordlistEnglish.english) {
|
|
||||||
const w = raw.toLowerCase().trim();
|
|
||||||
if (w.length === 2 && validTwo.has(w)) {
|
|
||||||
cleanWordSet.add(w);
|
|
||||||
} else if (w.length >= 3 && w.length <= 16 && /^[a-z]+$/.test(w)) {
|
|
||||||
cleanWordSet.add(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commonness tiers for intelligent word quality ranking
|
|
||||||
const t10 = new Set(wordlistEnglish['english/10'] || []);
|
|
||||||
const t20 = new Set(wordlistEnglish['english/20'] || []);
|
|
||||||
const t35 = new Set(wordlistEnglish['english/35'] || []);
|
|
||||||
const t50 = new Set(wordlistEnglish['english/50'] || []);
|
|
||||||
|
|
||||||
function getCommonnessScore(word) {
|
|
||||||
if (t10.has(word)) return 4;
|
|
||||||
if (t20.has(word)) return 3;
|
|
||||||
if (t35.has(word)) return 2;
|
|
||||||
if (t50.has(word)) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build prefix set for super fast solver pruning (up to 6 letters)
|
// Build prefix set for super fast solver pruning (up to 6 letters)
|
||||||
const prefixSet = new Set();
|
const prefixSet = new Set();
|
||||||
@@ -117,13 +89,8 @@ export function solveBoard(quartets, maxWords = 30) {
|
|||||||
return a.word.localeCompare(b.word);
|
return a.word.localeCompare(b.word);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cap words at maxWords (default: 30). Target quartiles and single-tile words
|
// Cap words at maxWords (default: 30) while keeping a fair share of 2-tile words.
|
||||||
// (tiles that are words on their own) always keep their spot.
|
words = capWordList(words, maxWords);
|
||||||
if (maxWords && words.length > maxWords) {
|
|
||||||
const reserved = words.filter(w => w.isTargetQuartile || w.tileCount === 1);
|
|
||||||
const rest = words.filter(w => !w.isTargetQuartile && w.tileCount !== 1);
|
|
||||||
words = [...reserved, ...rest.slice(0, Math.max(0, maxWords - reserved.length))];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final display sorting: Target quartiles first, then points desc, then alphabetical
|
// Final display sorting: Target quartiles first, then points desc, then alphabetical
|
||||||
words.sort((a, b) => {
|
words.sort((a, b) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function createScoreRankBar({ onOpenWordsDrawer, onOpenReveal }) {
|
|||||||
const rankInfo = state.getRank();
|
const rankInfo = state.getRank();
|
||||||
const quartilesSolved = state.getTargetQuartilesSolvedCount();
|
const quartilesSolved = state.getTargetQuartilesSolvedCount();
|
||||||
const totalWords = state.currentPuzzle ? (state.currentPuzzle.wordCount || state.currentPuzzle.words?.length || 0) : 0;
|
const totalWords = state.currentPuzzle ? (state.currentPuzzle.wordCount || state.currentPuzzle.words?.length || 0) : 0;
|
||||||
const foundWordsCount = state.foundWords.size;
|
const foundWordsCount = state.getListedFoundCount();
|
||||||
const isRevealed = !!state.isRevealed;
|
const isRevealed = !!state.isRevealed;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
|
|
||||||
const foundWordsList = Array.from(state.foundWords.values());
|
const foundWordsList = Array.from(state.foundWords.values());
|
||||||
const allPuzzleWords = state.currentPuzzle.words || [];
|
const allPuzzleWords = state.currentPuzzle.words || [];
|
||||||
|
const bonusFoundList = foundWordsList.filter(w => w.isBonus);
|
||||||
|
const listedFoundCount = foundWordsList.length - bonusFoundList.length;
|
||||||
const isRevealed = !!state.isRevealed;
|
const isRevealed = !!state.isRevealed;
|
||||||
|
|
||||||
// Active view: if user picked one use it, otherwise default to 'all' if revealed, 'found' if not
|
// Active view: if user picked one use it, otherwise default to 'all' if revealed, 'found' if not
|
||||||
const activeView = viewMode !== null ? viewMode : (isRevealed ? 'all' : 'found');
|
const activeView = viewMode !== null ? viewMode : (isRevealed ? 'all' : 'found');
|
||||||
|
|
||||||
const sourceWords = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList;
|
const sourceWords = (activeView === 'all' && isRevealed) ? [...allPuzzleWords, ...bonusFoundList] : foundWordsList;
|
||||||
|
|
||||||
// Filter words according to current tab
|
// Filter words according to current tab
|
||||||
let displayWords = [...sourceWords];
|
let displayWords = [...sourceWords];
|
||||||
@@ -40,7 +42,7 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
return a.word.localeCompare(b.word);
|
return a.word.localeCompare(b.word);
|
||||||
});
|
});
|
||||||
|
|
||||||
const countingSource = (activeView === 'all' && isRevealed) ? allPuzzleWords : foundWordsList;
|
const countingSource = sourceWords;
|
||||||
const quartilesCount = countingSource.filter(w => w.tileCount === 4).length;
|
const quartilesCount = countingSource.filter(w => w.tileCount === 4).length;
|
||||||
const threeTilesCount = countingSource.filter(w => w.tileCount === 3).length;
|
const threeTilesCount = countingSource.filter(w => w.tileCount === 3).length;
|
||||||
const twoTilesCount = countingSource.filter(w => w.tileCount === 2).length;
|
const twoTilesCount = countingSource.filter(w => w.tileCount === 2).length;
|
||||||
@@ -65,8 +67,8 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;">
|
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;">
|
||||||
${
|
${
|
||||||
isRevealed
|
isRevealed
|
||||||
? `<strong>${foundWordsList.length}</strong> of ${allPuzzleWords.length} found (${state.score} pts) • ${allPuzzleWords.length - foundWordsList.length} revealed`
|
? `<strong>${listedFoundCount}</strong> of ${allPuzzleWords.length} found (${state.score} pts) • ${allPuzzleWords.length - listedFoundCount} revealed${bonusFoundList.length ? ` • +${bonusFoundList.length} bonus` : ''}`
|
||||||
: `<strong>${foundWordsList.length}</strong> of ${allPuzzleWords.length} words found (${state.score} pts)`
|
: `<strong>${listedFoundCount}</strong> of ${allPuzzleWords.length} words found${bonusFoundList.length ? ` +${bonusFoundList.length} bonus` : ''} (${state.score} pts)`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +149,9 @@ export function createWordsDrawer({ onRevealAll }) {
|
|||||||
${isQuartile ? '<span title="Target Quartile (8 pts)"> ⭐</span>' : ''}
|
${isQuartile ? '<span title="Target Quartile (8 pts)"> ⭐</span>' : ''}
|
||||||
${
|
${
|
||||||
isFound
|
isFound
|
||||||
? '<span class="status-chip found">Found</span>'
|
? (w.isBonus
|
||||||
|
? '<span class="status-chip found" title="Valid word not on the main list">Bonus</span>'
|
||||||
|
: '<span class="status-chip found">Found</span>')
|
||||||
: '<span class="status-chip revealed">Revealed</span>'
|
: '<span class="status-chip revealed">Revealed</span>'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
3435
src/data/puzzles.js
3435
src/data/puzzles.js
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { DICTIONARY_SET, PREFIX_SET } from '../data/lexicon.js';
|
import { DICTIONARY_SET, PREFIX_SET } from '../data/lexicon.js';
|
||||||
|
import { capWordList } from './word-cap.js';
|
||||||
import { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo } from './constants.js';
|
import { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo } from './constants.js';
|
||||||
|
|
||||||
export { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo };
|
export { POINTS_BY_TILE_COUNT, QUARTILE_COMPLETION_BONUS, EXPERT_RANK_POINTS, RANK_TIERS, getRankInfo };
|
||||||
@@ -77,13 +78,9 @@ export function solveBoard(quartets, maxWords = 30) {
|
|||||||
return a.word.localeCompare(b.word);
|
return a.word.localeCompare(b.word);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cap at maxWords (default: 30). Target quartiles and single-tile words
|
// Cap at maxWords (default: 30) while keeping a fair share of 2-tile words.
|
||||||
// (tiles that are words on their own) always keep their spot.
|
// Valid words beyond the cap are still accepted in play as bonus words.
|
||||||
if (maxWords && words.length > maxWords) {
|
words = capWordList(words, maxWords);
|
||||||
const reserved = words.filter(w => w.isTargetQuartile || w.tileCount === 1);
|
|
||||||
const rest = words.filter(w => !w.isTargetQuartile && w.tileCount !== 1);
|
|
||||||
words = [...reserved, ...rest.slice(0, Math.max(0, maxWords - reserved.length))];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final display sort: Quartiles first, then by points descending, then alphabetically
|
// Final display sort: Quartiles first, then by points descending, then alphabetically
|
||||||
words.sort((a, b) => {
|
words.sort((a, b) => {
|
||||||
|
|||||||
27
src/engine/word-cap.js
Normal file
27
src/engine/word-cap.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Caps a sorted candidate word list to `maxWords` entries.
|
||||||
|
*
|
||||||
|
* - Target quartiles and single-tile words always keep their spot.
|
||||||
|
* - 2-tile words are guaranteed roughly half of the remaining slots (when
|
||||||
|
* available) so long 3/4-tile combos can't crowd them out.
|
||||||
|
* - Remaining slots are filled in the incoming sort order.
|
||||||
|
*
|
||||||
|
* Words cut by the cap are still accepted during play as bonus words.
|
||||||
|
*/
|
||||||
|
export function capWordList(words, maxWords = 30) {
|
||||||
|
if (!maxWords || words.length <= maxWords) return words;
|
||||||
|
|
||||||
|
const reserved = words.filter(w => w.isTargetQuartile || w.tileCount === 1);
|
||||||
|
const others = words.filter(w => !w.isTargetQuartile && w.tileCount !== 1);
|
||||||
|
const slots = Math.max(0, maxWords - reserved.length);
|
||||||
|
|
||||||
|
const twoTile = others.filter(w => w.tileCount === 2);
|
||||||
|
const quota = Math.min(twoTile.length, Math.ceil(slots / 2));
|
||||||
|
const picked = new Set(twoTile.slice(0, quota));
|
||||||
|
for (const w of others) {
|
||||||
|
if (picked.size >= slots) break;
|
||||||
|
picked.add(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...reserved, ...others.filter(w => picked.has(w))];
|
||||||
|
}
|
||||||
@@ -209,7 +209,7 @@ async function initApp() {
|
|||||||
} else {
|
} else {
|
||||||
// Standard word
|
// Standard word
|
||||||
sound.playWordValid(result.points >= 4 ? 3 : result.points);
|
sound.playWordValid(result.points >= 4 ? 3 : result.points);
|
||||||
gameState.notifyToast(`+${result.points} pts: ${result.word.toUpperCase()}`, 'success', 2200);
|
gameState.notifyToast(`+${result.points} pts: ${result.word.toUpperCase()}${result.isBonus ? ' (bonus word)' : ''}`, 'success', 2200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rank progression
|
// Check rank progression
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CURATED_PUZZLES } from '../data/puzzles.js';
|
import { CURATED_PUZZLES } from '../data/puzzles.js';
|
||||||
import { getRankInfo, QUARTILE_COMPLETION_BONUS } from '../engine/constants.js';
|
import { getRankInfo, QUARTILE_COMPLETION_BONUS, POINTS_BY_TILE_COUNT } from '../engine/constants.js';
|
||||||
|
import { DICTIONARY_SET } from '../data/lexicon.js';
|
||||||
|
|
||||||
class GameStateManager {
|
class GameStateManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -168,13 +169,21 @@ class GameStateManager {
|
|||||||
const filteredEntries = [];
|
const filteredEntries = [];
|
||||||
let recalculatedScore = 0;
|
let recalculatedScore = 0;
|
||||||
for (const [w, entry] of parsed.foundWords) {
|
for (const [w, entry] of parsed.foundWords) {
|
||||||
const matchedWord = validWordsMap.get(w.toLowerCase());
|
const key = w.toLowerCase();
|
||||||
|
const matchedWord = validWordsMap.get(key);
|
||||||
if (matchedWord) {
|
if (matchedWord) {
|
||||||
entry.points = matchedWord.points;
|
entry.points = matchedWord.points;
|
||||||
entry.tileCount = matchedWord.tileCount;
|
entry.tileCount = matchedWord.tileCount;
|
||||||
entry.tileSegments = matchedWord.tileSegments;
|
entry.tileSegments = matchedWord.tileSegments;
|
||||||
filteredEntries.push([w.toLowerCase(), entry]);
|
entry.isBonus = false;
|
||||||
|
filteredEntries.push([key, entry]);
|
||||||
recalculatedScore += matchedWord.points;
|
recalculatedScore += matchedWord.points;
|
||||||
|
} else if (entry && DICTIONARY_SET.has(key)) {
|
||||||
|
// Valid word beyond the displayed 30-word list
|
||||||
|
entry.isBonus = true;
|
||||||
|
entry.points = POINTS_BY_TILE_COUNT[entry.tileCount] || 1;
|
||||||
|
filteredEntries.push([key, entry]);
|
||||||
|
recalculatedScore += entry.points;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.foundWords = new Map(filteredEntries);
|
this.foundWords = new Map(filteredEntries);
|
||||||
@@ -434,7 +443,13 @@ class GameStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Look up in current puzzle's valid words list
|
// Look up in current puzzle's valid words list
|
||||||
const match = this.currentPuzzle.words.find(w => w.word.toLowerCase() === constructedWord);
|
const listed = this.currentPuzzle.words.find(w => w.word.toLowerCase() === constructedWord);
|
||||||
|
const tileCount = this.activeTileIds.length;
|
||||||
|
|
||||||
|
// Any real word built from the tiles counts, even if it was cut from the
|
||||||
|
// displayed 30-word list (accepted as a bonus word)
|
||||||
|
const isBonus = !listed && DICTIONARY_SET.has(constructedWord);
|
||||||
|
const match = listed || (isBonus ? { points: POINTS_BY_TILE_COUNT[tileCount] || 1 } : null);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
this.notifyToast(`"${constructedWord.toUpperCase()}" is not in the word list`, 'error');
|
this.notifyToast(`"${constructedWord.toUpperCase()}" is not in the word list`, 'error');
|
||||||
@@ -446,7 +461,6 @@ class GameStateManager {
|
|||||||
const tile = this.boardTiles.find(t => t.id === id);
|
const tile = this.boardTiles.find(t => t.id === id);
|
||||||
return tile ? tile.text : '';
|
return tile ? tile.text : '';
|
||||||
});
|
});
|
||||||
const tileCount = this.activeTileIds.length;
|
|
||||||
const isTargetQuartile = this.currentPuzzle.targetQuartiles.some(t => t.toLowerCase() === constructedWord);
|
const isTargetQuartile = this.currentPuzzle.targetQuartiles.some(t => t.toLowerCase() === constructedWord);
|
||||||
const isQuartile = tileCount === 4;
|
const isQuartile = tileCount === 4;
|
||||||
|
|
||||||
@@ -457,6 +471,7 @@ class GameStateManager {
|
|||||||
tileSegments,
|
tileSegments,
|
||||||
isTargetQuartile,
|
isTargetQuartile,
|
||||||
isQuartile,
|
isQuartile,
|
||||||
|
isBonus,
|
||||||
foundAt: Date.now()
|
foundAt: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -496,6 +511,7 @@ class GameStateManager {
|
|||||||
points: match.points,
|
points: match.points,
|
||||||
isQuartile,
|
isQuartile,
|
||||||
isTargetQuartile,
|
isTargetQuartile,
|
||||||
|
isBonus,
|
||||||
quartilesFound: quartilesCount,
|
quartilesFound: quartilesCount,
|
||||||
bonusUnlocked
|
bonusUnlocked
|
||||||
};
|
};
|
||||||
@@ -511,11 +527,20 @@ class GameStateManager {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Number of found words that are on the puzzle's displayed list (excludes bonus words)
|
||||||
|
getListedFoundCount() {
|
||||||
|
let count = 0;
|
||||||
|
for (const entry of this.foundWords.values()) {
|
||||||
|
if (!entry.isBonus) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
// Get rank data
|
// Get rank data
|
||||||
getRank() {
|
getRank() {
|
||||||
return getRankInfo(
|
return getRankInfo(
|
||||||
this.score,
|
this.score,
|
||||||
this.foundWords.size,
|
this.getListedFoundCount(),
|
||||||
this.currentPuzzle ? this.currentPuzzle.wordCount : 0
|
this.currentPuzzle ? this.currentPuzzle.wordCount : 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user