puzlink 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +35 -0
  3. package/dist/data/answerLengths.d.ts +10 -0
  4. package/dist/data/answerLengths.d.ts.map +1 -0
  5. package/dist/data/answerLengths.js +63 -0
  6. package/dist/data/answerLengths.js.map +1 -0
  7. package/dist/data/categories/compass.d.ts +3 -0
  8. package/dist/data/categories/compass.d.ts.map +1 -0
  9. package/dist/data/categories/compass.js +11 -0
  10. package/dist/data/categories/compass.js.map +1 -0
  11. package/dist/data/categories/countryAlpha2.d.ts +3 -0
  12. package/dist/data/categories/countryAlpha2.d.ts.map +1 -0
  13. package/dist/data/categories/countryAlpha2.js +252 -0
  14. package/dist/data/categories/countryAlpha2.js.map +1 -0
  15. package/dist/data/categories/countryAlpha3.d.ts +3 -0
  16. package/dist/data/categories/countryAlpha3.d.ts.map +1 -0
  17. package/dist/data/categories/countryAlpha3.js +252 -0
  18. package/dist/data/categories/countryAlpha3.js.map +1 -0
  19. package/dist/data/categories/daysOfTheWeek.d.ts +3 -0
  20. package/dist/data/categories/daysOfTheWeek.d.ts.map +1 -0
  21. package/dist/data/categories/daysOfTheWeek.js +10 -0
  22. package/dist/data/categories/daysOfTheWeek.js.map +1 -0
  23. package/dist/data/categories/elementSymbols.d.ts +3 -0
  24. package/dist/data/categories/elementSymbols.d.ts.map +1 -0
  25. package/dist/data/categories/elementSymbols.js +121 -0
  26. package/dist/data/categories/elementSymbols.js.map +1 -0
  27. package/dist/data/categories/greekLetters.d.ts +3 -0
  28. package/dist/data/categories/greekLetters.d.ts.map +1 -0
  29. package/dist/data/categories/greekLetters.js +27 -0
  30. package/dist/data/categories/greekLetters.js.map +1 -0
  31. package/dist/data/categories/months.d.ts +3 -0
  32. package/dist/data/categories/months.d.ts.map +1 -0
  33. package/dist/data/categories/months.js +15 -0
  34. package/dist/data/categories/months.js.map +1 -0
  35. package/dist/data/categories/natoAlphabet.d.ts +3 -0
  36. package/dist/data/categories/natoAlphabet.d.ts.map +1 -0
  37. package/dist/data/categories/natoAlphabet.js +29 -0
  38. package/dist/data/categories/natoAlphabet.js.map +1 -0
  39. package/dist/data/categories/numbers.d.ts +3 -0
  40. package/dist/data/categories/numbers.d.ts.map +1 -0
  41. package/dist/data/categories/numbers.js +16 -0
  42. package/dist/data/categories/numbers.js.map +1 -0
  43. package/dist/data/categories/romanNumerals.d.ts +3 -0
  44. package/dist/data/categories/romanNumerals.d.ts.map +1 -0
  45. package/dist/data/categories/romanNumerals.js +134 -0
  46. package/dist/data/categories/romanNumerals.js.map +1 -0
  47. package/dist/data/categories/solfege.d.ts +3 -0
  48. package/dist/data/categories/solfege.d.ts.map +1 -0
  49. package/dist/data/categories/solfege.js +11 -0
  50. package/dist/data/categories/solfege.js.map +1 -0
  51. package/dist/data/categories/usStateAbbreviations.d.ts +3 -0
  52. package/dist/data/categories/usStateAbbreviations.d.ts.map +1 -0
  53. package/dist/data/categories/usStateAbbreviations.js +53 -0
  54. package/dist/data/categories/usStateAbbreviations.js.map +1 -0
  55. package/dist/data/categories.d.ts +10 -0
  56. package/dist/data/categories.d.ts.map +1 -0
  57. package/dist/data/categories.js +31 -0
  58. package/dist/data/categories.js.map +1 -0
  59. package/dist/data/knownLogProbs.d.ts +6 -0
  60. package/dist/data/knownLogProbs.d.ts.map +1 -0
  61. package/dist/data/knownLogProbs.js +2975 -0
  62. package/dist/data/knownLogProbs.js.map +1 -0
  63. package/dist/data/morse.d.ts +2 -0
  64. package/dist/data/morse.d.ts.map +1 -0
  65. package/dist/data/morse.js +29 -0
  66. package/dist/data/morse.js.map +1 -0
  67. package/dist/data/scrabble.d.ts +2 -0
  68. package/dist/data/scrabble.d.ts.map +1 -0
  69. package/dist/data/scrabble.js +29 -0
  70. package/dist/data/scrabble.js.map +1 -0
  71. package/dist/features/index.d.ts +32 -0
  72. package/dist/features/index.d.ts.map +1 -0
  73. package/dist/features/index.js +79 -0
  74. package/dist/features/index.js.map +1 -0
  75. package/dist/features/letterCount.d.ts +7 -0
  76. package/dist/features/letterCount.d.ts.map +1 -0
  77. package/dist/features/letterCount.js +121 -0
  78. package/dist/features/letterCount.js.map +1 -0
  79. package/dist/features/letterSequence.d.ts +7 -0
  80. package/dist/features/letterSequence.d.ts.map +1 -0
  81. package/dist/features/letterSequence.js +155 -0
  82. package/dist/features/letterSequence.js.map +1 -0
  83. package/dist/features/logProbCache.d.ts +16 -0
  84. package/dist/features/logProbCache.d.ts.map +1 -0
  85. package/dist/features/logProbCache.js +36 -0
  86. package/dist/features/logProbCache.js.map +1 -0
  87. package/dist/features/other.d.ts +4 -0
  88. package/dist/features/other.d.ts.map +1 -0
  89. package/dist/features/other.js +190 -0
  90. package/dist/features/other.js.map +1 -0
  91. package/dist/features/substring.d.ts +3 -0
  92. package/dist/features/substring.d.ts.map +1 -0
  93. package/dist/features/substring.js +146 -0
  94. package/dist/features/substring.js.map +1 -0
  95. package/dist/features/wordplay.d.ts +7 -0
  96. package/dist/features/wordplay.d.ts.map +1 -0
  97. package/dist/features/wordplay.js +387 -0
  98. package/dist/features/wordplay.js.map +1 -0
  99. package/dist/index.d.ts +4 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +3 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/lib/affixDistribution.d.ts +26 -0
  104. package/dist/lib/affixDistribution.d.ts.map +1 -0
  105. package/dist/lib/affixDistribution.js +105 -0
  106. package/dist/lib/affixDistribution.js.map +1 -0
  107. package/dist/lib/counter.d.ts +23 -0
  108. package/dist/lib/counter.d.ts.map +1 -0
  109. package/dist/lib/counter.js +55 -0
  110. package/dist/lib/counter.js.map +1 -0
  111. package/dist/lib/distribution.d.ts +40 -0
  112. package/dist/lib/distribution.d.ts.map +1 -0
  113. package/dist/lib/distribution.js +176 -0
  114. package/dist/lib/distribution.js.map +1 -0
  115. package/dist/lib/lengthDistribution.d.ts +30 -0
  116. package/dist/lib/lengthDistribution.d.ts.map +1 -0
  117. package/dist/lib/lengthDistribution.js +137 -0
  118. package/dist/lib/lengthDistribution.js.map +1 -0
  119. package/dist/lib/letterBitset.d.ts +49 -0
  120. package/dist/lib/letterBitset.d.ts.map +1 -0
  121. package/dist/lib/letterBitset.js +101 -0
  122. package/dist/lib/letterBitset.js.map +1 -0
  123. package/dist/lib/letterDistribution.d.ts +60 -0
  124. package/dist/lib/letterDistribution.d.ts.map +1 -0
  125. package/dist/lib/letterDistribution.js +230 -0
  126. package/dist/lib/letterDistribution.js.map +1 -0
  127. package/dist/lib/letterIndices.d.ts +13 -0
  128. package/dist/lib/letterIndices.d.ts.map +1 -0
  129. package/dist/lib/letterIndices.js +41 -0
  130. package/dist/lib/letterIndices.js.map +1 -0
  131. package/dist/lib/logCounter.d.ts +23 -0
  132. package/dist/lib/logCounter.d.ts.map +1 -0
  133. package/dist/lib/logCounter.js +49 -0
  134. package/dist/lib/logCounter.js.map +1 -0
  135. package/dist/lib/logNum.d.ts +36 -0
  136. package/dist/lib/logNum.d.ts.map +1 -0
  137. package/dist/lib/logNum.js +193 -0
  138. package/dist/lib/logNum.js.map +1 -0
  139. package/dist/lib/memoize.d.ts +5 -0
  140. package/dist/lib/memoize.d.ts.map +1 -0
  141. package/dist/lib/memoize.js +104 -0
  142. package/dist/lib/memoize.js.map +1 -0
  143. package/dist/lib/util.d.ts +30 -0
  144. package/dist/lib/util.d.ts.map +1 -0
  145. package/dist/lib/util.js +111 -0
  146. package/dist/lib/util.js.map +1 -0
  147. package/dist/lib/wordlist.d.ts +66 -0
  148. package/dist/lib/wordlist.d.ts.map +1 -0
  149. package/dist/lib/wordlist.js +166 -0
  150. package/dist/lib/wordlist.js.map +1 -0
  151. package/dist/linkers/index.d.ts +34 -0
  152. package/dist/linkers/index.d.ts.map +1 -0
  153. package/dist/linkers/index.js +25 -0
  154. package/dist/linkers/index.js.map +1 -0
  155. package/dist/linkers/indexing.d.ts +5 -0
  156. package/dist/linkers/indexing.d.ts.map +1 -0
  157. package/dist/linkers/indexing.js +152 -0
  158. package/dist/linkers/indexing.js.map +1 -0
  159. package/dist/linkers/length.d.ts +5 -0
  160. package/dist/linkers/length.d.ts.map +1 -0
  161. package/dist/linkers/length.js +101 -0
  162. package/dist/linkers/length.js.map +1 -0
  163. package/dist/linkers/letterDistribution.d.ts +4 -0
  164. package/dist/linkers/letterDistribution.d.ts.map +1 -0
  165. package/dist/linkers/letterDistribution.js +46 -0
  166. package/dist/linkers/letterDistribution.js.map +1 -0
  167. package/dist/linkers/other.d.ts +5 -0
  168. package/dist/linkers/other.d.ts.map +1 -0
  169. package/dist/linkers/other.js +90 -0
  170. package/dist/linkers/other.js.map +1 -0
  171. package/dist/parse.d.ts +8 -0
  172. package/dist/parse.d.ts.map +1 -0
  173. package/dist/parse.js +23 -0
  174. package/dist/parse.js.map +1 -0
  175. package/dist/puzlink.d.ts +84 -0
  176. package/dist/puzlink.d.ts.map +1 -0
  177. package/dist/puzlink.js +59 -0
  178. package/dist/puzlink.js.map +1 -0
  179. package/package.json +57 -0
  180. package/src/data/answerLengths.ts +63 -0
  181. package/src/data/categories/README.md +3 -0
  182. package/src/data/categories/compass.ts +1 -0
  183. package/src/data/categories/countryAlpha2.ts +251 -0
  184. package/src/data/categories/countryAlpha3.ts +251 -0
  185. package/src/data/categories/daysOfTheWeek.ts +1 -0
  186. package/src/data/categories/elementSymbols.ts +120 -0
  187. package/src/data/categories/greekLetters.ts +26 -0
  188. package/src/data/categories/months.ts +14 -0
  189. package/src/data/categories/natoAlphabet.ts +28 -0
  190. package/src/data/categories/numbers.ts +15 -0
  191. package/src/data/categories/romanNumerals.ts +133 -0
  192. package/src/data/categories/solfege.ts +1 -0
  193. package/src/data/categories/txt/compass.txt +8 -0
  194. package/src/data/categories/txt/daysOfTheWeek.txt +7 -0
  195. package/src/data/categories/txt/elementSymbols.txt +118 -0
  196. package/src/data/categories/txt/greekLetters.txt +24 -0
  197. package/src/data/categories/txt/months.txt +12 -0
  198. package/src/data/categories/txt/natoAlphabet.txt +26 -0
  199. package/src/data/categories/txt/numbers.txt +13 -0
  200. package/src/data/categories/txt/solfege.txt +8 -0
  201. package/src/data/categories/txt/usStateAbbreviations.txt +50 -0
  202. package/src/data/categories/usStateAbbreviations.ts +52 -0
  203. package/src/data/categories.ts +42 -0
  204. package/src/data/knownLogProbs.ts +2992 -0
  205. package/src/data/morse.ts +28 -0
  206. package/src/data/scrabble.ts +28 -0
  207. package/src/features/index.ts +120 -0
  208. package/src/features/letterCount.ts +174 -0
  209. package/src/features/letterSequence.ts +222 -0
  210. package/src/features/logProbCache.ts +48 -0
  211. package/src/features/other.ts +214 -0
  212. package/src/features/substring.ts +173 -0
  213. package/src/features/wordplay.ts +428 -0
  214. package/src/index.ts +3 -0
  215. package/src/lib/affixDistribution.ts +70 -0
  216. package/src/lib/counter.ts +71 -0
  217. package/src/lib/distribution.ts +162 -0
  218. package/src/lib/lengthDistribution.ts +108 -0
  219. package/src/lib/letterBitset.ts +123 -0
  220. package/src/lib/letterDistribution.ts +236 -0
  221. package/src/lib/letterIndices.ts +51 -0
  222. package/src/lib/logCounter.ts +74 -0
  223. package/src/lib/logNum.ts +193 -0
  224. package/src/lib/memoize.ts +136 -0
  225. package/src/lib/testUtils.ts +1 -0
  226. package/src/lib/util.ts +150 -0
  227. package/src/lib/wordlist.ts +162 -0
  228. package/src/linkers/index.ts +56 -0
  229. package/src/linkers/indexing.ts +194 -0
  230. package/src/linkers/length.ts +122 -0
  231. package/src/linkers/other.ts +117 -0
  232. package/src/parse.ts +20 -0
  233. package/src/puzlink.ts +141 -0
@@ -0,0 +1,428 @@
1
+ import { LETTERS } from "../lib/letterDistribution.js";
2
+ import {
3
+ caesar,
4
+ enumerate,
5
+ interval,
6
+ mapProduct,
7
+ windows,
8
+ } from "../lib/util.js";
9
+ import type { Feature } from "./index.js";
10
+
11
+ function prependWith(letter: string): Feature {
12
+ return {
13
+ name: `can prepend ${letter}`,
14
+ property: (slug, { wordlist }) => {
15
+ const prepended = `${letter}${slug}`;
16
+ return wordlist.isWord(prepended)
17
+ ? `${letter} + ${slug} = ${prepended}`
18
+ : null;
19
+ },
20
+ };
21
+ }
22
+
23
+ function prependAny(): Feature {
24
+ return {
25
+ name: "can prepend 1",
26
+ property: (slug, { wordlist }) => {
27
+ const prepended = wordlist.filterWords(
28
+ Array.from(LETTERS).map((letter) => `${letter}${slug}`),
29
+ );
30
+ return prepended.length === 0
31
+ ? null
32
+ : `1 + ${slug} = ${prepended.join(", ")}`;
33
+ },
34
+ };
35
+ }
36
+
37
+ function appendWith(letter: string): Feature {
38
+ return {
39
+ name: `can append ${letter}`,
40
+ property: (slug, { wordlist }) => {
41
+ const appended = `${slug}${letter}`;
42
+ return wordlist.isWord(appended)
43
+ ? `${slug} + ${letter} = ${appended}`
44
+ : null;
45
+ },
46
+ };
47
+ }
48
+
49
+ function appendAny(): Feature {
50
+ return {
51
+ name: "can append 1",
52
+ property: (slug, { wordlist }) => {
53
+ const appended = wordlist.filterWords(
54
+ Array.from(LETTERS).map((letter) => `${slug}${letter}`),
55
+ );
56
+ return appended.length === 0
57
+ ? null
58
+ : `${slug} + 1 = ${appended.join(", ")}`;
59
+ },
60
+ };
61
+ }
62
+
63
+ function insertWith(letter: string): Feature {
64
+ return {
65
+ name: `can insert ${letter}`,
66
+ property: (slug, { wordlist }) => {
67
+ const allInserted = [];
68
+ for (let i = 0; i <= slug.length; i++) {
69
+ allInserted.push(`${slug.slice(0, i)}${letter}${slug.slice(i)}`);
70
+ }
71
+ const inserted = wordlist.filterWords(allInserted);
72
+ return inserted.length === 0
73
+ ? null
74
+ : `${slug} insert ${letter} = ${inserted.join(", ")}`;
75
+ },
76
+ };
77
+ }
78
+
79
+ function insertAny(): Feature {
80
+ return {
81
+ name: "can insert 1",
82
+ property: (slug, { wordlist }) => {
83
+ const allInserted = [];
84
+ for (let i = 0; i <= slug.length; i++) {
85
+ for (const letter of LETTERS) {
86
+ allInserted.push(`${slug.slice(0, i)}${letter}${slug.slice(i)}`);
87
+ }
88
+ }
89
+ const inserted = wordlist.filterWords(allInserted);
90
+ return inserted.length === 0
91
+ ? null
92
+ : `${slug} insert 1 = ${inserted.join(", ")}`;
93
+ },
94
+ };
95
+ }
96
+
97
+ function behead(): Feature {
98
+ return {
99
+ name: "can behead 1",
100
+ property: (slug, { wordlist }) => {
101
+ const beheaded = slug.slice(1);
102
+ return wordlist.isWord(beheaded)
103
+ ? `${slug} behead 1 = ${beheaded}`
104
+ : null;
105
+ },
106
+ };
107
+ }
108
+
109
+ function curtail(): Feature {
110
+ return {
111
+ name: "can curtail 1",
112
+ property: (slug, { wordlist }) => {
113
+ const curtailed = slug.slice(0, -1);
114
+ return wordlist.isWord(curtailed)
115
+ ? `${slug} curtail 1 = ${curtailed}`
116
+ : null;
117
+ },
118
+ };
119
+ }
120
+
121
+ function deleteWith(letter: string): Feature {
122
+ return {
123
+ name: `can delete ${letter}`,
124
+ property: (slug, { wordlist }) => {
125
+ const allDeleted = [];
126
+ for (const [i, c] of enumerate(slug)) {
127
+ if (c === letter) {
128
+ allDeleted.push(`${slug.slice(0, i)}${slug.slice(i + 1)}`);
129
+ }
130
+ }
131
+ const deleted = wordlist.filterWords(allDeleted);
132
+ return deleted.length === 0
133
+ ? null
134
+ : `${slug} delete ${letter} = ${deleted.join(", ")}`;
135
+ },
136
+ };
137
+ }
138
+
139
+ function deleteAny(): Feature {
140
+ return {
141
+ name: "can delete 1",
142
+ property: (slug, { wordlist }) => {
143
+ const allDeleted = [];
144
+ for (const [i] of enumerate(slug)) {
145
+ allDeleted.push(`${slug.slice(0, i)}${slug.slice(i + 1)}`);
146
+ }
147
+ const deleted = wordlist.filterWords(allDeleted);
148
+ return deleted.length === 0
149
+ ? null
150
+ : `${slug} delete 1 = ${deleted.join(", ")}`;
151
+ },
152
+ };
153
+ }
154
+
155
+ function takeOddOrEven(): Feature {
156
+ return {
157
+ name: "can take odd or even letters",
158
+ property: (slug, { wordlist }) => {
159
+ const odd = Array.from(slug)
160
+ .filter((_, i) => i % 2 === 1)
161
+ .join("");
162
+ const even = Array.from(slug)
163
+ .filter((_, i) => i % 2 === 0)
164
+ .join("");
165
+ const isOdd = wordlist.isWord(odd);
166
+ const isEven = wordlist.isWord(even);
167
+ if (!isOdd && !isEven) {
168
+ return null;
169
+ }
170
+ if (isOdd && isEven) {
171
+ return `${slug} take odd = ${odd}; take even = ${even}`;
172
+ }
173
+ return isOdd
174
+ ? `${slug} take odd = ${odd}`
175
+ : `${slug} take even = ${even}`;
176
+ },
177
+ };
178
+ }
179
+
180
+ function changeTo(letter: string): Feature {
181
+ return {
182
+ name: `can change to ${letter}`,
183
+ property: (slug, { wordlist }) => {
184
+ const allChanged = [];
185
+ for (const [i, c] of enumerate(slug)) {
186
+ if (c !== letter) {
187
+ allChanged.push(`${slug.slice(0, i)}${letter}${slug.slice(i + 1)}`);
188
+ }
189
+ }
190
+ const changed = wordlist.filterWords(allChanged);
191
+ return changed.length === 0
192
+ ? null
193
+ : `${slug} change to ${letter} = ${changed.join(", ")}`;
194
+ },
195
+ };
196
+ }
197
+
198
+ function changeAny(): Feature {
199
+ return {
200
+ name: "can change 1",
201
+ property: (slug, { wordlist }) => {
202
+ const allChanged = [];
203
+ for (const [i, c] of enumerate(slug)) {
204
+ for (const letter of LETTERS) {
205
+ if (c !== letter) {
206
+ allChanged.push(`${slug.slice(0, i)}${letter}${slug.slice(i + 1)}`);
207
+ }
208
+ }
209
+ }
210
+ const changed = wordlist.filterWords(allChanged);
211
+ return changed.length === 0
212
+ ? null
213
+ : `${slug} change 1 = ${changed.join(", ")}`;
214
+ },
215
+ };
216
+ }
217
+
218
+ function reverse(): Feature {
219
+ return {
220
+ name: "can reverse",
221
+ property: (slug, { wordlist }) => {
222
+ const reversed = slug.split("").reverse().join("");
223
+ return wordlist.isWord(reversed)
224
+ ? `${slug} reversed = ${reversed}`
225
+ : null;
226
+ },
227
+ };
228
+ }
229
+
230
+ function rotate(): Feature {
231
+ return {
232
+ name: "can rotate",
233
+ property: (slug, { wordlist }) => {
234
+ const candidates: [candidate: string, n: number][] = [];
235
+ for (let n = 1; n < slug.length; n++) {
236
+ candidates.push([slug.slice(n) + slug.slice(0, n), n]);
237
+ }
238
+ const rotates = wordlist.filterWordsUnder(candidates, (t) => t[0]);
239
+ if (rotates.length === 0) {
240
+ return null;
241
+ }
242
+ const [first, ...rest] = rotates;
243
+ const [rotated, n] = first!;
244
+ const nStr =
245
+ n > slug.length / 2 ? (n - slug.length).toString() : n.toString();
246
+ return `${slug} rotate ${nStr} = ${rotated}${
247
+ rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
248
+ }`;
249
+ },
250
+ };
251
+ }
252
+
253
+ function swapAdjacent(): Feature {
254
+ return {
255
+ name: "can swap adjacent letters",
256
+ property: (slug, { wordlist }) => {
257
+ const candidates: [candidate: string, i: number][] = [];
258
+ for (const [i, [a, b]] of enumerate(windows(slug, 2))) {
259
+ if (a !== b) {
260
+ candidates.push([
261
+ `${slug.slice(0, i)}${b}${a}${slug.slice(i + 2)}`,
262
+ i,
263
+ ]);
264
+ }
265
+ }
266
+ const swapped = wordlist.filterWordsUnder(candidates, ([w]) => w);
267
+ if (swapped.length === 0) {
268
+ return null;
269
+ }
270
+ const [first, ...rest] = swapped;
271
+ const swapIndices = `${(first![1] + 1).toString()}, ${(first![1] + 2).toString()}`;
272
+ return `${slug} swap ${swapIndices} = ${first![0]}${
273
+ rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
274
+ }`;
275
+ },
276
+ };
277
+ }
278
+
279
+ function swapEnds(): Feature {
280
+ return {
281
+ name: "can swap ends",
282
+ property: (slug, { wordlist }) => {
283
+ if (slug.length < 2) {
284
+ return null;
285
+ }
286
+ const candidate = `${slug.at(-1)!}${slug.slice(1, -1)}${slug.at(0)!}`;
287
+ return wordlist.isWord(candidate)
288
+ ? `${slug} swap ends = ${candidate}`
289
+ : null;
290
+ },
291
+ };
292
+ }
293
+
294
+ function anagram(): Feature {
295
+ return {
296
+ name: "is anagram",
297
+ property: (slug, { wordlist }) => {
298
+ const anagrams = wordlist.anagrams(slug);
299
+ return anagrams.length === 0
300
+ ? null
301
+ : `${slug} anagrammed = ${anagrams.join(", ")}`;
302
+ },
303
+ };
304
+ }
305
+
306
+ function transaddWith(letter: string): Feature {
307
+ return {
308
+ name: `has transadd ${letter}`,
309
+ property: (slug, { wordlist }) => {
310
+ const transadds = wordlist.anagrams(`${slug}${letter}`, {
311
+ loose: true,
312
+ });
313
+ return transadds.length === 0
314
+ ? null
315
+ : `${slug} transadd ${letter} = ${transadds.join(", ")}`;
316
+ },
317
+ };
318
+ }
319
+
320
+ function transaddAny(): Feature {
321
+ return {
322
+ name: "has transadd 1",
323
+ property: (slug, { wordlist }) => {
324
+ const allTransadds = [];
325
+ for (const letter of LETTERS) {
326
+ for (const transadd of wordlist.anagrams(`${slug}${letter}`, {
327
+ loose: true,
328
+ })) {
329
+ allTransadds.push(transadd);
330
+ }
331
+ }
332
+ const transadds = wordlist.filterWords(allTransadds);
333
+ return transadds.length === 0
334
+ ? null
335
+ : `${slug} transadd 1 = ${transadds.join(", ")}`;
336
+ },
337
+ };
338
+ }
339
+
340
+ function transdeleteWith(letter: string): Feature {
341
+ return {
342
+ name: `has transdelete ${letter}`,
343
+ property: (slug, { wordlist }) => {
344
+ if (!slug.includes(letter)) {
345
+ return null;
346
+ }
347
+ const transdeletes = wordlist.anagrams(slug.replace(letter, ""), {
348
+ loose: true,
349
+ });
350
+ return transdeletes.length === 0
351
+ ? null
352
+ : `${slug} transdelete ${letter} = ${transdeletes.join(", ")}`;
353
+ },
354
+ };
355
+ }
356
+
357
+ function transdeleteAny(): Feature {
358
+ return {
359
+ name: "has transdelete 1",
360
+ property: (slug, { wordlist }) => {
361
+ const allTransdeletes = [];
362
+ for (const letter of new Set(slug)) {
363
+ for (const transdelete of wordlist.anagrams(slug.replace(letter, ""), {
364
+ loose: true,
365
+ })) {
366
+ allTransdeletes.push(transdelete);
367
+ }
368
+ }
369
+ const transdeletes = wordlist.filterWords(allTransdeletes);
370
+ return transdeletes.length === 0
371
+ ? null
372
+ : `${slug} transdelete 1 = ${transdeletes.join(", ")}`;
373
+ },
374
+ };
375
+ }
376
+
377
+ function caesarShift(): Feature {
378
+ return {
379
+ name: "has caesar shift",
380
+ property: (slug, { wordlist }) => {
381
+ const candidates: [candidate: string, n: number][] = [
382
+ ...interval(-12, -1),
383
+ ...interval(1, 13),
384
+ ].map((n) => [caesar(slug, n), n]);
385
+ const shifted = wordlist.filterWordsUnder(candidates, (t) => t[0]);
386
+ if (shifted.length === 0) {
387
+ return null;
388
+ }
389
+ const [first, ...rest] = shifted;
390
+ const [shiftedSlug, n] = first!;
391
+ return `${slug} caesar shift ${n.toString()} = ${shiftedSlug}${
392
+ rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
393
+ }`;
394
+ },
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Features for wordplay: slugs that form a word when applying some sort of
400
+ * transformation.
401
+ */
402
+ export function wordplayFeatures(): Feature[] {
403
+ return [
404
+ ...mapProduct(prependWith, LETTERS),
405
+ prependAny(),
406
+ ...mapProduct(appendWith, LETTERS),
407
+ appendAny(),
408
+ ...mapProduct(insertWith, LETTERS),
409
+ insertAny(),
410
+ behead(),
411
+ curtail(),
412
+ ...mapProduct(deleteWith, LETTERS),
413
+ deleteAny(),
414
+ takeOddOrEven(),
415
+ ...mapProduct(changeTo, LETTERS),
416
+ changeAny(),
417
+ reverse(),
418
+ rotate(),
419
+ swapAdjacent(),
420
+ swapEnds(),
421
+ anagram(),
422
+ ...mapProduct(transaddWith, LETTERS),
423
+ transaddAny(),
424
+ ...mapProduct(transdeleteWith, LETTERS),
425
+ transdeleteAny(),
426
+ caesarShift(),
427
+ ];
428
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { parse } from "./parse.js";
2
+ export { Puzlink } from "./puzlink.js";
3
+ export type { Link, LinkOptions } from "./puzlink.js";
@@ -0,0 +1,70 @@
1
+ import { Distribution } from "./distribution.js";
2
+ import { LogNum } from "./logNum.js";
3
+ import { memoize } from "./memoize.js";
4
+ import { interval } from "./util.js";
5
+
6
+ class BaseAffixDistribution {
7
+ /** Map from affix length to distribution of affixes of that length. */
8
+ private readonly dist = new Map<number, Distribution<string>>();
9
+
10
+ constructor(affix: "prefix" | "suffix", wordlist: string[]) {
11
+ let maxLength = 0;
12
+ for (const word of wordlist) {
13
+ maxLength = Math.max(maxLength, word.length);
14
+ }
15
+
16
+ const affixes = new Map<number, string[]>();
17
+
18
+ for (const i of interval(1, maxLength)) {
19
+ affixes.set(i, []);
20
+ }
21
+
22
+ for (const word of wordlist) {
23
+ for (let i = 1; i <= word.length; i++) {
24
+ affixes
25
+ .get(i)!
26
+ .push(affix === "prefix" ? word.slice(0, i) : word.slice(-i));
27
+ }
28
+ }
29
+
30
+ for (const [length, items] of affixes) {
31
+ this.dist.set(length, Distribution.from(items));
32
+ }
33
+ }
34
+
35
+ /** Distribution of affixes of a given length. */
36
+ get(length: number): Distribution<string> {
37
+ return this.dist.get(length) ?? Distribution.from([]);
38
+ }
39
+
40
+ /** Vowel distribution for affixes of a given length. */
41
+ @memoize(1)
42
+ private vowelDist(length: number): Distribution<string> {
43
+ return this.get(length).map((s) => {
44
+ return s.replaceAll(/[aeiou]/g, "V").replaceAll(/[a-z]/g, "C");
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Probability that k affixes of a given length start with the same vowel
50
+ * pattern.
51
+ */
52
+ @memoize(2)
53
+ probEqualVowelPattern(k: number, length: number): LogNum {
54
+ return this.vowelDist(length).probEqual(k);
55
+ }
56
+ }
57
+
58
+ /** Info about the prefix distribution of a wordlist. */
59
+ export class PrefixDistribution extends BaseAffixDistribution {
60
+ constructor(wordlist: string[]) {
61
+ super("prefix", wordlist);
62
+ }
63
+ }
64
+
65
+ /** Info about the suffix distribution of a wordlist. */
66
+ export class SuffixDistribution extends BaseAffixDistribution {
67
+ constructor(wordlist: string[]) {
68
+ super("suffix", wordlist);
69
+ }
70
+ }
@@ -0,0 +1,71 @@
1
+ /** A map from items to counts. */
2
+ export class Counter<T extends PropertyKey> {
3
+ private readonly counts: Map<T, number>;
4
+
5
+ constructor(counts?: Map<T, number>, totalCache?: number) {
6
+ this.counts = counts ?? new Map<T, number>();
7
+ this.totalCache = totalCache;
8
+ }
9
+
10
+ static from(data: string): Counter<string>;
11
+ static from<T extends PropertyKey>(data: Iterable<T>): Counter<T>;
12
+ static from(data: string | Iterable<PropertyKey>) {
13
+ const counts = new Map<PropertyKey, number>();
14
+ let total = 0;
15
+
16
+ for (const item of data) {
17
+ counts.set(item, (counts.get(item) ?? 0) + 1);
18
+ total += 1;
19
+ }
20
+
21
+ return new Counter(counts, total);
22
+ }
23
+
24
+ /** Add the given item to the counter. */
25
+ addOne(item: T): void {
26
+ this.counts.set(item, (this.counts.get(item) ?? 0) + 1);
27
+ if (this.totalCache !== undefined) {
28
+ this.totalCache += 1;
29
+ }
30
+ }
31
+
32
+ /** Add the given items to the counter. */
33
+ addMany(data: Iterable<T>): void {
34
+ this.totalCache = undefined;
35
+ for (const item of data) {
36
+ this.counts.set(item, (this.counts.get(item) ?? 0) + 1);
37
+ }
38
+ }
39
+
40
+ /** The number of distinct items. */
41
+ get distinct(): number {
42
+ return this.counts.size;
43
+ }
44
+
45
+ private totalCache: number | undefined;
46
+
47
+ /** The total number of all items. */
48
+ get total(): number {
49
+ return (this.totalCache ??= Array.from(this.counts.values()).reduce(
50
+ (a, b) => a + b,
51
+ 0,
52
+ ));
53
+ }
54
+
55
+ /** The count of the given item. */
56
+ get(item: T): number {
57
+ return this.counts.get(item) ?? 0;
58
+ }
59
+
60
+ /** Returns an iterable of [item, count] pairs. */
61
+ entries(): IterableIterator<[T, number]> {
62
+ return this.counts.entries();
63
+ }
64
+
65
+ /** Returns a list of items that satisfy the given predicate. */
66
+ filterKeys(fn: (item: T, count: number) => boolean): T[] {
67
+ return Array.from(this.counts.entries())
68
+ .filter(([item, count]) => fn(item, count))
69
+ .map(([item]) => item);
70
+ }
71
+ }