LINGUABASE — MAKE A SPELLING GAME (machine-readable method file) ================================================================ Companion to https://www.linguabase.org/ — a free, public-domain (CC0) semantic dataset of English. This file is self-contained: tables, method, measured results, and a complete worked example. Written to be read by an LLM asked to adapt the method to a different word game. THE DATA -------- Download (parquet or TSV, identical on three mirrors, linked from linguabase.org/download.html). Tables used here, 5 of 13: vocabulary word, core_rank rank 1 = most familiar of 400,000 scores_labels word, content_rating (G..X), sensitivity (1-5, 5 safest) word_families word, relatives list of related words clues word, 26 clue columns one-word up to expert register definitions word, definition one readable paragraph Note: the shipped files are not sorted by rank and the TSVs carry a header row — sort on core_rank before taking prefixes. THE METHOD ---------- A game idea becomes a measurement in four moves: 1 filter: single words, a-z only, rating G or PG, sensitivity > 3 2 dedupe: drop any plural whose singular ranks better 3 write each game rule as a filter over the ranked list 4 count the survivors; move the thresholds; count again Every number below was produced this way — measured, never estimated. WORKED EXAMPLE — an anagram ladder game --------------------------------------- One pool of 7 letter tiles must contain a 4-, a 5- and a 6-letter word (the easy words, unrelated to the answer and to each other, together covering all 7 tiles) plus the 7-letter answer. No other 7-letter word from the same tiles may rank within twice the answer's rank (a reachable rival). Easy words capped at rank 50,000; answers at 70,000. Measured funnel (top 200,000 rows of the ranked vocabulary): 200,000 -> 65,985 clean single words -> 9,560 seven-letter -> 5,371 within the answer cap -> 4,340 after the plural filter -> 1,778 with all three easy-word lengths -> 1,760 with full tile coverage -> 1,510 with no reachable rival = 1,510 puzzles = 4.1 years of daily levels Answer-cap sensitivity (same pipeline, cap varied): 10k: 524 (1.4y) · 20k: 813 · 30k: 903 · 40k: 1,102 50k: 1,254 · 60k: 1,386 · 70k: 1,510 (4.1y) Easy-cap x answer-cap matrix highlights (puzzle counts): easy 12k: 473..1,789 · easy 25k: 627..2,432 · easy 40k: 752..3,077 easy 50k: 827..3,372 · easy 60k: 876..3,601 (answer caps 25k..400k) THE BUILD SCRIPT (complete, runnable) ------------------------------------- # prepare_words.py — filter, test, space out, clue; writes levels.tsv + display.json import pandas as pd, json from collections import Counter EASY_CAP, FINALE_CAP = 50_000, 70_000 # the two numbers you edit; all else follows v = pd.read_parquet('vocabulary.core.parquet') s = pd.read_parquet('scores_labels.core.parquet').set_index('word') fam = pd.read_parquet('word_families.core.parquet').set_index('word').relatives clu = pd.read_parquet('clues.core.parquet').set_index('word') dfn = pd.read_parquet('definitions.core.parquet').set_index('word').definition clean = v[~v.is_mwe & v.word.str.fullmatch('[a-z]{4,7}') # 1 · the two lists & v.word.map(s.content_rating).isin(['G', 'PG']) & (v.word.map(s.sensitivity) > 3)].copy() rank = dict(zip(clean.word, clean.core_rank)) kin = lambda w: set(fam.get(w, [])) clean = clean[[not any(w in (b+'s', b+'es') and rank.get(b, 9e9) < n # plurals only: for b in kin(w)) # ANSWERS goes, for w, n in zip(clean.word, clean.core_rank)]] # ANSWER stays finales = clean[(clean.word.str.len() == 7) & (clean.core_rank <= FINALE_CAP)].sort_values('core_rank') sig = lambda w: ''.join(sorted(w)) fits = lambda w, pool: not Counter(w) - Counter(pool) overlap = lambda a, b: sum((Counter(a) & Counter(b)).values()) def test(fin): # 2 · the pool is the answer, sorted pool = sig(fin) # 3 · the validity test sp = [w for w in clean.word if w != fin and fits(w, pool)] easy = {} for L in (4, 5, 6): # most familiar easy word of each length c = [w for w in sp if len(w) == L and rank[w] <= EASY_CAP and w not in kin(fin) and w not in fin] if not c: return None # a length missing: throw it back easy[L] = min(c, key=rank.__getitem__) if set(pool) - set(''.join(easy.values())): return None # uncovered tile if any(len(w) == 7 and rank[w] < 2 * rank[fin] for w in sp): return None # a rival close enough to reach return [easy[4], easy[5], easy[6]], sp # sp minus the easy words = the bonuses def fresh(pool, history): # 4 · the overlap filter for age, old in enumerate(reversed(history)): n = overlap(pool, old) if n >= 6 or (n == 5 and age < 100) or (n == 4 and age < 10): return False return True levels, display, history = [], {}, [] for fin in finales.word: got = test(fin) if not got or not fresh(sig(fin), history): continue easy, sp = got lvl = len(levels) + 1 tier = 'expert' if lvl > 500 else 'learner' # 5 · later levels take the expert angle main = clu.at[fin, 'clue_EXP' if tier == 'expert' else 'clue_LRN'] hint = clu.at[fin, 'clue_LRN' if tier == 'expert' else 'clue_DEF'] if any(r in main + hint for r in kin(fin)): continue # clue contains a relative of the answer history.append(sig(fin)) levels.append((lvl, sig(fin).upper(), '|'.join(easy), fin, main, hint, rank[fin], max(rank[w] for w in easy), tier)) for w in [fin] + sp: display[w] = dfn[w] cols = 'level_no pool easy_words finale clue_main clue_hint finale_rank easy_deepest clue_tier'.split() pd.DataFrame(levels, columns=cols).to_csv('levels.tsv', sep='\t', index=False) json.dump(display, open('display.json', 'w'), indent=1) print(f'{len(levels):,} levels') AT PLAY TIME ------------ Load levels.tsv and display.json once. Three functions run the game: spellable(w): letter-multiset check against the pool (the only buzz) play(w): UNSPELLABLE | SOLVED | EASY | BONUS | UNKNOWN (a full-length rival like SPECTER pays as a BONUS) meaning(w): display.json lookup — one line per find, the paragraph at level end WHAT COUNTING DOES NOT SETTLE ----------------------------- Whether rank 50,000 matches what your players find familiar (playtesting); which finales you would cut on sight (reading them); the easy words skew -ed and -s forms, and blocking those costs about 23%. YOUR GAME --------- To adapt: state your mechanic and its rules, then (1) write each rule as a filter over the ranked vocabulary, reusing the filters above; (2) count survivors at three threshold settings; (3) show three sample puzzles at each. The tables are CC0 — no permission needed, commercial or otherwise.