From e4ec5f21743d1d6bb2a9fcc12d3245459c86e07c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Jul 2026 21:47:59 +0000 Subject: [PATCH 1/2] fix: restrict codas and rimes to letters legal at word end (drop v coda, ech rime) Co-authored-by: Bu Kinoshita --- src/utils/phonemes.spec.ts | 12 ++++++++++++ src/utils/phonemes.ts | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/utils/phonemes.spec.ts b/src/utils/phonemes.spec.ts index 7cbf638..c14280a 100644 --- a/src/utils/phonemes.spec.ts +++ b/src/utils/phonemes.spec.ts @@ -29,4 +29,16 @@ describe('phoneme arrays', () => { it('RIMES contains only vowel-initial entries', () => { expect(RIMES.filter((rime) => !/^[aeiou]/.test(rime))).toEqual([]); }); + + it('CODAS contains only letters legal at word end', () => { + expect(CODAS.filter((coda) => /[jqwhv]/.test(coda))).toEqual([]); + }); + + it('RIMES contains only entries legal at word end', () => { + expect(RIMES.filter((rime) => /[jqwhv]$/.test(rime))).toEqual([]); + }); + + it('TAILS contains only entries legal at word end', () => { + expect(TAILS.filter((tail) => /[jqwhv]$/.test(tail))).toEqual([]); + }); }); diff --git a/src/utils/phonemes.ts b/src/utils/phonemes.ts index 2a7f450..f93098a 100644 --- a/src/utils/phonemes.ts +++ b/src/utils/phonemes.ts @@ -132,7 +132,6 @@ export const RIMES = Object.freeze([ 'ild', 'old', 'uld', - 'ech', 'amb', ]); @@ -175,7 +174,6 @@ export const CODAS = Object.freeze([ 'g', 'b', 'f', - 'v', 'z', ]); From 2b738d6597591d238ba4c3034b4131d5a9ee460f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Jul 2026 21:47:59 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20make=20segment=20joins=20seam-aware?= =?UTF-8?q?=20=E2=80=94=20repair=20illegal=20clusters=20and=20vowel=20pile?= =?UTF-8?q?ups=20with=20linking=20phonemes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bu Kinoshita --- src/utils/can-join.spec.ts | 76 ++++++++++++++++++ src/utils/can-join.ts | 29 +++++++ src/utils/clusters.spec.ts | 31 ++++++++ src/utils/clusters.ts | 137 ++++++++++++++++++++++++++++++++ src/utils/generator.spec.ts | 14 ++++ src/utils/join-segments.spec.ts | 102 ++++++++++++++++++++++++ src/utils/join-segments.ts | 47 +++++++++++ src/utils/pattern.spec.ts | 44 ++++++++++ src/utils/pattern.ts | 70 +++++----------- 9 files changed, 502 insertions(+), 48 deletions(-) create mode 100644 src/utils/can-join.spec.ts create mode 100644 src/utils/can-join.ts create mode 100644 src/utils/clusters.spec.ts create mode 100644 src/utils/clusters.ts create mode 100644 src/utils/join-segments.spec.ts create mode 100644 src/utils/join-segments.ts diff --git a/src/utils/can-join.spec.ts b/src/utils/can-join.spec.ts new file mode 100644 index 0000000..9f99eda --- /dev/null +++ b/src/utils/can-join.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { canJoin } from './can-join'; + +describe('canJoin', () => { + it('allows joining onto an empty left segment', () => { + expect(canJoin('', 'kra')).toBe(true); + expect(canJoin('', 'a')).toBe(true); + }); + + it('allows joining an empty right segment', () => { + expect(canJoin('mor', '')).toBe(true); + }); + + it('rejects illegal consonant clusters at word start', () => { + expect(canJoin('t', 'kra')).toBe(false); + expect(canJoin('th', 'drel')).toBe(false); + expect(canJoin('sh', 'sa')).toBe(false); + expect(canJoin('ch', 'xa')).toBe(false); + expect(canJoin('z', 'fi')).toBe(false); + expect(canJoin('n', 'na')).toBe(false); + }); + + it('accepts legal consonant clusters at word start', () => { + expect(canJoin('t', 'ra')).toBe(true); + expect(canJoin('th', 'ren')).toBe(true); + expect(canJoin('s', 'la')).toBe(true); + expect(canJoin('c', 'ra')).toBe(true); + expect(canJoin('ph', 'ra')).toBe(true); + }); + + it('rejects illegal consonant clusters mid-word', () => { + expect(canJoin('morauz', 'gri')).toBe(false); + expect(canJoin('velas', 'yan')).toBe(false); + expect(canJoin('fenax', 'ka')).toBe(false); + }); + + it('accepts legal consonant clusters mid-word', () => { + expect(canJoin('vel', 'ka')).toBe(true); + expect(canJoin('bron', 'drel')).toBe(true); + expect(canJoin('morauz', 'za')).toBe(true); + expect(canJoin('fenax', 'ta')).toBe(true); + expect(canJoin('velan', 'yan')).toBe(true); + }); + + it('rejects vowel seams that create a run of three or more vowels', () => { + expect(canJoin('velau', 'ilo')).toBe(false); + expect(canJoin('minea', 'oro')).toBe(false); + expect(canJoin('lunia', 'ei')).toBe(false); + }); + + it('accepts vowel seams that keep vowel runs at two', () => { + expect(canJoin('vela', 'ilo')).toBe(true); + expect(canJoin('min', 'oro')).toBe(true); + expect(canJoin('feno', 'une')).toBe(true); + }); + + it('rejects un-English double vowels at the seam', () => { + expect(canJoin('vela', 'ava')).toBe(false); + expect(canJoin('mini', 'ilo')).toBe(false); + expect(canJoin('lunu', 'ulo')).toBe(false); + }); + + it('requires a single vowel after qu', () => { + expect(canJoin('qu', 'ara')).toBe(true); + expect(canJoin('qu', 'a')).toBe(true); + expect(canJoin('qu', 'kra')).toBe(false); + expect(canJoin('qu', 'ee')).toBe(false); + expect(canJoin('velaqu', 'e')).toBe(true); + expect(canJoin('velaqu', 'ta')).toBe(false); + }); + + it('accepts consonant-vowel and vowel-consonant seams', () => { + expect(canJoin('vel', 'ara')).toBe(true); + expect(canJoin('vela', 'kra')).toBe(true); + }); +}); diff --git a/src/utils/can-join.ts b/src/utils/can-join.ts new file mode 100644 index 0000000..d1bfe44 --- /dev/null +++ b/src/utils/can-join.ts @@ -0,0 +1,29 @@ +import { INITIAL_CLUSTERS, MEDIAL_CLUSTERS } from './clusters'; + +const BANNED_VOWEL_PAIRS: ReadonlySet = new Set(['aa', 'ii', 'uu']); + +const isVowel = (char: string): boolean => 'aeiou'.includes(char); + +const leadingVowelRun = (s: string): string => s.match(/^[aeiou]+/)?.[0] ?? ''; + +const trailingVowelRun = (s: string): string => s.match(/[aeiou]+$/)?.[0] ?? ''; + +const leadingConsonantRun = (s: string): string => s.match(/^[^aeiou]+/)?.[0] ?? ''; + +const trailingConsonantRun = (s: string): string => s.match(/[^aeiou]+$/)?.[0] ?? ''; + +export const canJoin = (left: string, right: string): boolean => { + if (left === '' || right === '') return true; + const lastChar = left[left.length - 1]; + const firstChar = right[0]; + if (BANNED_VOWEL_PAIRS.has(`${lastChar}${firstChar}`)) return false; + if (left.endsWith('qu') && !isVowel(firstChar)) return false; + if (isVowel(lastChar) && isVowel(firstChar)) { + return trailingVowelRun(left).length + leadingVowelRun(right).length <= 2; + } + if (isVowel(lastChar) || isVowel(firstChar)) return true; + const leftRun = trailingConsonantRun(left); + const seamCluster = leftRun + leadingConsonantRun(right); + const seamAtWordStart = leftRun.length === left.length; + return seamAtWordStart ? INITIAL_CLUSTERS.has(seamCluster) : MEDIAL_CLUSTERS.has(seamCluster); +}; diff --git a/src/utils/clusters.spec.ts b/src/utils/clusters.spec.ts new file mode 100644 index 0000000..3a29ab7 --- /dev/null +++ b/src/utils/clusters.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { INITIAL_CLUSTERS, MEDIAL_CLUSTERS } from './clusters'; + +describe('cluster whitelists', () => { + it.each([ + ['INITIAL_CLUSTERS', INITIAL_CLUSTERS], + ['MEDIAL_CLUSTERS', MEDIAL_CLUSTERS], + ])('%s contains only lowercase consonant sequences', (_, clusters) => { + expect([...clusters].filter((cluster) => !/^[b-df-hj-np-tv-z]+$/.test(cluster))).toEqual([]); + }); + + it.each([ + ['INITIAL_CLUSTERS', INITIAL_CLUSTERS], + ['MEDIAL_CLUSTERS', MEDIAL_CLUSTERS], + ])('%s contains only clusters of two or three consonants', (_, clusters) => { + expect([...clusters].filter((cluster) => cluster.length < 2 || cluster.length > 3)).toEqual([]); + }); + + it('INITIAL_CLUSTERS excludes clusters English never starts words with', () => { + expect(INITIAL_CLUSTERS.has('tk')).toBe(false); + expect(INITIAL_CLUSTERS.has('zf')).toBe(false); + expect(INITIAL_CLUSTERS.has('vr')).toBe(false); + expect(INITIAL_CLUSTERS.has('nn')).toBe(false); + }); + + it('MEDIAL_CLUSTERS excludes clusters English never uses mid-word', () => { + expect(MEDIAL_CLUSTERS.has('zg')).toBe(false); + expect(MEDIAL_CLUSTERS.has('zgr')).toBe(false); + expect(MEDIAL_CLUSTERS.has('xk')).toBe(false); + }); +}); diff --git a/src/utils/clusters.ts b/src/utils/clusters.ts new file mode 100644 index 0000000..80adf01 --- /dev/null +++ b/src/utils/clusters.ts @@ -0,0 +1,137 @@ +export const INITIAL_CLUSTERS: ReadonlySet = new Set([ + 'bl', + 'br', + 'cl', + 'cr', + 'dr', + 'dw', + 'fl', + 'fr', + 'gl', + 'gr', + 'kl', + 'kr', + 'pl', + 'pr', + 'sc', + 'sk', + 'sl', + 'sm', + 'sn', + 'sp', + 'st', + 'sw', + 'tr', + 'tw', + 'wr', + 'ch', + 'ph', + 'sh', + 'th', + 'wh', + 'chr', + 'phr', + 'shr', + 'thr', + 'scr', + 'spl', + 'spr', + 'str', +]); + +export const MEDIAL_CLUSTERS: ReadonlySet = new Set([ + 'bl', + 'br', + 'cl', + 'cr', + 'dr', + 'fl', + 'fr', + 'gl', + 'gr', + 'kl', + 'kr', + 'pl', + 'pr', + 'sc', + 'sk', + 'sl', + 'sm', + 'sn', + 'sp', + 'st', + 'tl', + 'tr', + 'vr', + 'ch', + 'ph', + 'sh', + 'th', + 'wh', + 'lb', + 'lc', + 'ld', + 'lf', + 'lg', + 'lk', + 'lm', + 'lp', + 'ls', + 'lt', + 'lv', + 'ly', + 'mb', + 'mp', + 'nc', + 'nd', + 'nf', + 'ng', + 'nj', + 'nk', + 'nl', + 'ns', + 'nt', + 'nv', + 'ny', + 'nz', + 'rb', + 'rc', + 'rd', + 'rf', + 'rg', + 'rk', + 'rl', + 'rm', + 'rn', + 'rp', + 'rs', + 'rt', + 'rv', + 'ry', + 'xt', + 'bb', + 'dd', + 'ff', + 'gg', + 'll', + 'mm', + 'nn', + 'pp', + 'rr', + 'ss', + 'tt', + 'zz', + 'ldr', + 'ltr', + 'mbr', + 'mpr', + 'ndr', + 'nfl', + 'ngr', + 'nkr', + 'nsl', + 'ntr', + 'rtr', + 'str', + 'xtr', +]); diff --git a/src/utils/generator.spec.ts b/src/utils/generator.spec.ts index 5b19989..4d80613 100644 --- a/src/utils/generator.spec.ts +++ b/src/utils/generator.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest'; +import { INITIAL_CLUSTERS } from './clusters'; import { generateName } from './generator'; import { createRng } from './rng'; @@ -142,6 +143,19 @@ describe('generator', () => { expect(name).toBe(name.toLowerCase()); }); + it('never starts a name with an illegal consonant cluster', () => { + const config = { minLength: 6, maxLength: 10 }; + const offenders = Array.from({ length: 5000 }, (_, i) => { + const [name] = generateName(createRng(i), config); + return name; + }).filter((name) => { + const run = name.match(/^[^aeiou]+/)?.[0] ?? ''; + return run.length >= 2 && !INITIAL_CLUSTERS.has(run); + }); + + expect(offenders).toEqual([]); + }); + it('generates high-quality unique names', () => { const config = { minLength: 6, maxLength: 10 }; const names = new Set(); diff --git a/src/utils/join-segments.spec.ts b/src/utils/join-segments.spec.ts new file mode 100644 index 0000000..22420c1 --- /dev/null +++ b/src/utils/join-segments.spec.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; +import { joinSegments } from './join-segments'; +import { createRng } from './rng'; + +const sampleJoins = (choices: readonly (readonly string[])[], count: number): readonly string[] => + Array.from({ length: count }, (_, i) => { + const [result] = joinSegments(choices, createRng(i)); + return result; + }); + +describe('joinSegments', () => { + it('concatenates one segment from each choice list', () => { + const rng = createRng(42); + const [result] = joinSegments([['vel'], ['a'], ['ka']], rng); + + expect(result).toBe('velaka'); + }); + + it('returns an empty string for no choice lists', () => { + const rng = createRng(42); + const [result, finalRng] = joinSegments([], rng); + + expect(result).toBe(''); + expect(finalRng).toBe(rng); + }); + + it('joins legal seams without inserting anything', () => { + const results = sampleJoins([['t'], ['ra']], 20); + + expect(new Set(results)).toEqual(new Set(['tra'])); + }); + + it('repairs illegal consonant seams with a linking vowel', () => { + const results = sampleJoins([['t'], ['kra']], 200); + + expect(new Set(results)).toEqual(new Set(['takra', 'tekra', 'tikra', 'tokra', 'tukra'])); + }); + + it('repairs digraph collisions with a linking vowel', () => { + const results = sampleJoins([['th'], ['drel']], 200); + + expect(new Set(results)).toEqual( + new Set(['thadrel', 'thedrel', 'thidrel', 'thodrel', 'thudrel']), + ); + }); + + it('repairs vowel pileups with a linking consonant', () => { + const results = sampleJoins([['velau'], ['ilo']], 200); + + expect(new Set(results)).toEqual(new Set(['velaulilo', 'velaunilo', 'velaurilo'])); + }); + + it('repairs un-English double vowels with a linking consonant', () => { + const results = sampleJoins([['vela'], ['ava']], 200); + + expect(new Set(results)).toEqual(new Set(['velalava', 'velanava', 'velarava'])); + }); + + it('repairs consonants after qu with a linking vowel that is not u', () => { + const results = sampleJoins([['qu'], ['kra']], 200); + + expect(new Set(results)).toEqual(new Set(['quakra', 'quekra', 'quikra', 'quokra'])); + }); + + it('repairs vowel pileups after qu with a vowel-consonant bridge', () => { + const results = sampleJoins([['qu'], ['ee']], 400); + const bridged = /^qu[aeio][lnr]ee$/; + + expect(results.filter((result) => !bridged.test(result))).toEqual([]); + }); + + it('never produces a vowel run of three or more', () => { + const choices = [ + ['qu', 'vel', 't'], + ['a', 'ea', 'oo'], + ['ara', 'kra', 'ilo'], + ]; + + expect(sampleJoins(choices, 500).filter((result) => /[aeiou]{3,}/.test(result))).toEqual([]); + }); + + it('is deterministic with the same seed', () => { + const choices = [ + ['vel', 't', 'th'], + ['a', 'ea'], + ['kra', 'ara'], + ]; + + const [result1] = joinSegments(choices, createRng(777)); + const [result2] = joinSegments(choices, createRng(777)); + + expect(result1).toBe(result2); + }); + + it('returns a new RNG state after picking', () => { + const rng = createRng(100); + const [, nextRng] = joinSegments([['ba'], ['ne']], rng); + + expect(nextRng).toBeDefined(); + expect(nextRng).not.toBe(rng); + }); +}); diff --git a/src/utils/join-segments.ts b/src/utils/join-segments.ts new file mode 100644 index 0000000..6198da9 --- /dev/null +++ b/src/utils/join-segments.ts @@ -0,0 +1,47 @@ +import { canJoin } from './can-join'; +import { pick } from './pick'; +import type { RandomGenerator } from './rng'; + +const LINKING_VOWELS = Object.freeze(['a', 'e', 'i', 'o', 'u']); + +const LINKING_CONSONANTS = Object.freeze(['l', 'n', 'r']); + +const QU_LINKING_VOWELS = Object.freeze(['a', 'e', 'i', 'o']); + +const QU_LINKING_BRIDGES = Object.freeze([ + 'al', + 'an', + 'ar', + 'el', + 'en', + 'er', + 'il', + 'in', + 'ir', + 'ol', + 'on', + 'or', +]); + +const isVowel = (char: string): boolean => 'aeiou'.includes(char); + +const linkPool = (word: string, segment: string): readonly string[] => { + const firstChar = segment[0]; + if (word.endsWith('qu')) return isVowel(firstChar) ? QU_LINKING_BRIDGES : QU_LINKING_VOWELS; + const lastChar = word[word.length - 1]; + return isVowel(lastChar) && isVowel(firstChar) ? LINKING_CONSONANTS : LINKING_VOWELS; +}; + +export const joinSegments = ( + segmentChoices: readonly (readonly string[])[], + rng: RandomGenerator, +): readonly [string, RandomGenerator] => + segmentChoices.reduce( + ([word, state], choices) => { + const [segment, next] = pick(choices)(state); + if (canJoin(word, segment)) return [word + segment, next] as const; + const [link, after] = pick(linkPool(word, segment))(next); + return [word + link + segment, after] as const; + }, + ['', rng] as const, + ); diff --git a/src/utils/pattern.spec.ts b/src/utils/pattern.spec.ts index 906aa03..beb6b17 100644 --- a/src/utils/pattern.spec.ts +++ b/src/utils/pattern.spec.ts @@ -1,7 +1,19 @@ import { describe, expect, it } from 'vitest'; +import { INITIAL_CLUSTERS } from './clusters'; import { buildPattern, PATTERNS, type Pattern, WEIGHTS } from './pattern'; import { createRng } from './rng'; +const LEGAL_FINAL_CLUSTERS = new Set(['lk', 'rk', 'st', 'nt', 'ld', 'mb']); + +const sampleWords = (pattern: Pattern, count: number): readonly string[] => + Array.from({ length: count }, (_, i) => { + const [word] = buildPattern(pattern, createRng(i)); + return word; + }); + +const allSampledWords = (count: number): readonly string[] => + PATTERNS.flatMap((pattern) => sampleWords(pattern, count)); + describe('pattern', () => { describe('PATTERNS', () => { it('contains all expected pattern types', () => { @@ -258,6 +270,38 @@ describe('pattern', () => { } }); + describe('seam-aware joining', () => { + it('never starts a word with an illegal consonant cluster', () => { + const offenders = allSampledWords(500).filter((word) => { + const run = word.match(/^[^aeiou]+/)?.[0] ?? ''; + return run.length >= 2 && !INITIAL_CLUSTERS.has(run); + }); + + expect(offenders).toEqual([]); + }); + + it('never ends a word with an illegal consonant cluster', () => { + const offenders = allSampledWords(500).filter((word) => { + const run = word.match(/[^aeiou]+$/)?.[0] ?? ''; + return run.length >= 2 && !LEGAL_FINAL_CLUSTERS.has(run); + }); + + expect(offenders).toEqual([]); + }); + + it('never ends a word in j, q, w, h, or v', () => { + expect(allSampledWords(500).filter((word) => /[jqwhv]$/.test(word))).toEqual([]); + }); + + it('never produces a run of three or more vowels', () => { + expect(allSampledWords(500).filter((word) => /[aeiou]{3,}/.test(word))).toEqual([]); + }); + + it('never produces aa, ii, or uu', () => { + expect(allSampledWords(500).filter((word) => /aa|ii|uu/.test(word))).toEqual([]); + }); + }); + it('handles all patterns consistently', () => { const patterns: Pattern[] = [ 'ON+T', diff --git a/src/utils/pattern.ts b/src/utils/pattern.ts index 7bd424b..df2218f 100644 --- a/src/utils/pattern.ts +++ b/src/utils/pattern.ts @@ -1,7 +1,6 @@ +import { joinSegments } from './join-segments'; import { CODAS, NUCLEI, ONSETS, RIMES, TAILS } from './phonemes'; -import { pick } from './pick'; import type { RandomGenerator } from './rng'; -import { sequence } from './sequence'; export type Pattern = | 'ON+T' @@ -42,51 +41,26 @@ export const PATTERNS: readonly Pattern[] = [ export const WEIGHTS: readonly number[] = [4, 3, 1, 1, 1, 2, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2]; -export function buildPattern( +const PATTERN_SEGMENTS: Readonly> = { + 'ON+T': [ONSETS, TAILS], + 'ON+NU+T': [ONSETS, NUCLEI, TAILS], + CV: [ONSETS, NUCLEI], + CR: [ONSETS, RIMES], + CVC: [ONSETS, NUCLEI, CODAS], + CVT: [ONSETS, NUCLEI, TAILS], + CVCV: [ONSETS, NUCLEI, ONSETS, NUCLEI], + CVCT: [ONSETS, NUCLEI, CODAS, TAILS], + CVCR: [ONSETS, NUCLEI, ONSETS, RIMES], + CVCVC: [ONSETS, NUCLEI, ONSETS, NUCLEI, CODAS], + VCVCV: [NUCLEI, ONSETS, NUCLEI, ONSETS, NUCLEI], + CVCVT: [ONSETS, NUCLEI, ONSETS, NUCLEI, TAILS], + CVCTV: [ONSETS, NUCLEI, CODAS, TAILS, NUCLEI], + CVCVCT: [ONSETS, NUCLEI, ONSETS, NUCLEI, CODAS, TAILS], + VCVCVC: [NUCLEI, ONSETS, NUCLEI, ONSETS, NUCLEI, CODAS], + CVCVCV: [ONSETS, NUCLEI, ONSETS, NUCLEI, ONSETS, NUCLEI], +}; + +export const buildPattern = ( pattern: Pattern, rng: RandomGenerator, -): readonly [string, RandomGenerator] { - switch (pattern) { - case 'ON+T': - return sequence([pick(ONSETS), pick(TAILS)], rng); - case 'ON+NU+T': - return sequence([pick(ONSETS), pick(NUCLEI), pick(TAILS)], rng); - case 'CV': - return sequence([pick(ONSETS), pick(NUCLEI)], rng); - case 'CR': - return sequence([pick(ONSETS), pick(RIMES)], rng); - case 'CVC': - return sequence([pick(ONSETS), pick(NUCLEI), pick(CODAS)], rng); - case 'CVT': - return sequence([pick(ONSETS), pick(NUCLEI), pick(TAILS)], rng); - case 'CVCV': - return sequence([pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI)], rng); - case 'CVCT': - return sequence([pick(ONSETS), pick(NUCLEI), pick(CODAS), pick(TAILS)], rng); - case 'CVCR': - return sequence([pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(RIMES)], rng); - case 'CVCVC': - return sequence([pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(CODAS)], rng); - case 'VCVCV': - return sequence([pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI)], rng); - case 'CVCVT': - return sequence([pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(TAILS)], rng); - case 'CVCTV': - return sequence([pick(ONSETS), pick(NUCLEI), pick(CODAS), pick(TAILS), pick(NUCLEI)], rng); - case 'CVCVCT': - return sequence( - [pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(CODAS), pick(TAILS)], - rng, - ); - case 'VCVCVC': - return sequence( - [pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(CODAS)], - rng, - ); - case 'CVCVCV': - return sequence( - [pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI), pick(ONSETS), pick(NUCLEI)], - rng, - ); - } -} +): readonly [string, RandomGenerator] => joinSegments(PATTERN_SEGMENTS[pattern], rng);