windsor-bot 0.1.7 → 0.1.8
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.
- package/README.md +329 -77
- package/assets/wordsearches/animals.txt +404 -0
- package/assets/wordsearches/colors.txt +404 -0
- package/assets/wordsearches/food.txt +404 -0
- package/assets/wordsearches/fruits.txt +404 -0
- package/assets/wordsearches/household.txt +404 -0
- package/assets/wordsearches/music.txt +404 -0
- package/assets/wordsearches/nature.txt +404 -0
- package/assets/wordsearches/space.txt +404 -0
- package/assets/wordsearches/sports.txt +404 -0
- package/assets/wordsearches/travel.txt +404 -0
- package/dist/ai.js +6 -4
- package/dist/bot.js +87 -14
- package/dist/commands/index.js +2 -0
- package/dist/commands/pokemon.js +1 -1
- package/dist/commands/retry.js +17 -0
- package/dist/commands/wordsearch.js +111 -89
- package/dist/reactions.js +2 -1
- package/dist/web/app.bundle.js +23 -4
- package/dist/web/app.js +20 -1
- package/package.json +3 -2
|
@@ -1,104 +1,126 @@
|
|
|
1
|
+
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { Jimp, JimpMime, loadFont, measureText } from 'jimp';
|
|
1
6
|
import { tryExecCommandFunction } from './util.js';
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[0, 1], [1, 0], [1, 1], [1, -1],
|
|
30
|
-
[0, -1], [-1, 0], [-1, -1], [-1, 1],
|
|
31
|
-
];
|
|
32
|
-
function placeWord(grid, word) {
|
|
33
|
-
const shuffledDirs = [...DIRECTIONS].sort(() => Math.random() - 0.5);
|
|
34
|
-
for (const [dr, dc] of shuffledDirs) {
|
|
35
|
-
for (let attempt = 0; attempt < 30; attempt++) {
|
|
36
|
-
const row = Math.floor(Math.random() * SIZE);
|
|
37
|
-
const col = Math.floor(Math.random() * SIZE);
|
|
38
|
-
let fits = true;
|
|
39
|
-
for (let i = 0; i < word.length; i++) {
|
|
40
|
-
const r = row + dr * i;
|
|
41
|
-
const c = col + dc * i;
|
|
42
|
-
if (r < 0 || r >= SIZE || c < 0 || c >= SIZE) {
|
|
43
|
-
fits = false;
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
const cell = grid[r]?.[c];
|
|
47
|
-
if (cell && cell !== word[i]) {
|
|
48
|
-
fits = false;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PUZZLE_DIR = join(__dirname, '../../assets/wordsearches');
|
|
9
|
+
const FONT_DIR = new URL('../../node_modules/@jimp/plugin-print/dist/fonts/open-sans/', import.meta.url).pathname;
|
|
10
|
+
const WORDSEARCH_FONT = `${FONT_DIR}open-sans-32-black/open-sans-32-black.fnt`;
|
|
11
|
+
const IMAGE_WIDTH = 560;
|
|
12
|
+
const IMAGE_HEIGHT = 600;
|
|
13
|
+
const GRID_SIZE = 510;
|
|
14
|
+
const GRID_LEFT = (IMAGE_WIDTH - GRID_SIZE) / 2;
|
|
15
|
+
const GRID_TOP = 68;
|
|
16
|
+
const CELL_SIZE = GRID_SIZE / 15;
|
|
17
|
+
let puzzlesPromise;
|
|
18
|
+
async function loadPuzzles() {
|
|
19
|
+
const files = (await readdir(PUZZLE_DIR)).filter(file => file.endsWith('.txt')).sort();
|
|
20
|
+
const puzzles = [];
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const text = await readFile(join(PUZZLE_DIR, file), 'utf8');
|
|
23
|
+
const lines = text.split(/\r?\n/);
|
|
24
|
+
let theme = '';
|
|
25
|
+
let words = [];
|
|
26
|
+
let grid = [];
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i]?.trim() ?? '';
|
|
29
|
+
if (line.startsWith('THEME ')) {
|
|
30
|
+
theme = line.slice('THEME '.length).trim();
|
|
31
|
+
}
|
|
32
|
+
else if (line.startsWith('WORDS ')) {
|
|
33
|
+
words = line.slice('WORDS '.length).split(',').filter(Boolean);
|
|
51
34
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
35
|
+
else if (line === 'GRID') {
|
|
36
|
+
const gridLines = [];
|
|
37
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
38
|
+
const row = lines[j]?.trim() ?? '';
|
|
39
|
+
if (row === 'END')
|
|
40
|
+
break;
|
|
41
|
+
if (row)
|
|
42
|
+
gridLines.push(row);
|
|
58
43
|
}
|
|
59
|
-
|
|
44
|
+
grid = gridLines;
|
|
45
|
+
break;
|
|
60
46
|
}
|
|
61
47
|
}
|
|
48
|
+
if (!theme || words.length === 0 || grid.length === 0) {
|
|
49
|
+
throw new Error(`Invalid wordsearch asset: ${file}`);
|
|
50
|
+
}
|
|
51
|
+
puzzles.push({ theme, words, grid });
|
|
62
52
|
}
|
|
63
|
-
|
|
53
|
+
if (puzzles.length === 0)
|
|
54
|
+
throw new Error('No wordsearch assets found');
|
|
55
|
+
return puzzles;
|
|
64
56
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
async function renderWordsearchImage(puzzle) {
|
|
58
|
+
const font = await loadFont(WORDSEARCH_FONT);
|
|
59
|
+
const image = new Jimp({ width: IMAGE_WIDTH, height: IMAGE_HEIGHT, color: 0xffffffff });
|
|
60
|
+
const title = `Word Search - ${puzzle.theme}`;
|
|
61
|
+
image.print({
|
|
62
|
+
font,
|
|
63
|
+
x: (IMAGE_WIDTH - measureText(font, title)) / 2,
|
|
64
|
+
y: 22,
|
|
65
|
+
text: title,
|
|
66
|
+
});
|
|
67
|
+
const rows = puzzle.grid.map(row => row.split(/\s+/));
|
|
68
|
+
if (rows.length !== 15 || rows.some(row => row.length !== 15)) {
|
|
69
|
+
throw new Error(`Invalid ${puzzle.theme} wordsearch grid`);
|
|
70
|
+
}
|
|
71
|
+
for (let row = 0; row <= 15; row++) {
|
|
72
|
+
const y = Math.round(GRID_TOP + row * CELL_SIZE);
|
|
73
|
+
for (let x = GRID_LEFT; x <= GRID_LEFT + GRID_SIZE; x++) {
|
|
74
|
+
image.setPixelColor(0x000000ff, x, y);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
for (let col = 0; col <= 15; col++) {
|
|
78
|
+
const x = Math.round(GRID_LEFT + col * CELL_SIZE);
|
|
79
|
+
for (let y = GRID_TOP; y <= GRID_TOP + GRID_SIZE; y++) {
|
|
80
|
+
image.setPixelColor(0x000000ff, x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (let row = 0; row < 15; row++) {
|
|
84
|
+
const cells = rows[row];
|
|
85
|
+
if (!cells)
|
|
86
|
+
throw new Error(`Missing wordsearch row ${row + 1}`);
|
|
87
|
+
for (let col = 0; col < 15; col++) {
|
|
88
|
+
const text = cells[col];
|
|
89
|
+
if (!text)
|
|
90
|
+
throw new Error(`Missing wordsearch cell ${row + 1},${col + 1}`);
|
|
91
|
+
image.print({
|
|
92
|
+
font,
|
|
93
|
+
x: GRID_LEFT + col * CELL_SIZE + (CELL_SIZE - measureText(font, text)) / 2,
|
|
94
|
+
y: GRID_TOP + row * CELL_SIZE + 1,
|
|
95
|
+
text,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const imagePath = join(tmpdir(), `windsor-wordsearch-${process.pid}-${Date.now()}.png`);
|
|
100
|
+
await writeFile(imagePath, await image.getBuffer(JimpMime.png));
|
|
101
|
+
return imagePath;
|
|
78
102
|
}
|
|
79
103
|
async function printWordsearchWorker(_args, ctx) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
puzzlesPromise ??= loadPuzzles();
|
|
105
|
+
const puzzles = await puzzlesPromise;
|
|
106
|
+
const puzzle = puzzles[Math.floor(Math.random() * puzzles.length)];
|
|
107
|
+
if (!puzzle)
|
|
108
|
+
throw new Error('No wordsearch puzzle available');
|
|
109
|
+
const imagePath = await renderWordsearchImage(puzzle);
|
|
110
|
+
try {
|
|
111
|
+
const job = {
|
|
112
|
+
urls: [],
|
|
113
|
+
lines: [
|
|
114
|
+
'Find these words:',
|
|
115
|
+
puzzle.words.join(' '),
|
|
116
|
+
],
|
|
117
|
+
iconPath: imagePath,
|
|
118
|
+
};
|
|
119
|
+
await ctx.printJob(job);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await unlink(imagePath);
|
|
88
123
|
}
|
|
89
|
-
fillGrid(grid);
|
|
90
|
-
const lines = [
|
|
91
|
-
...renderGrid(grid),
|
|
92
|
-
'',
|
|
93
|
-
'Find these words:',
|
|
94
|
-
placed.join(' '),
|
|
95
|
-
];
|
|
96
|
-
const job = {
|
|
97
|
-
urls: [],
|
|
98
|
-
header: `Word Search: ${theme.name}`,
|
|
99
|
-
lines,
|
|
100
|
-
};
|
|
101
|
-
await ctx.printJob(job);
|
|
102
124
|
return {
|
|
103
125
|
kind: "pass"
|
|
104
126
|
};
|