windsor-bot 0.1.6 → 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/sudoku-easy.txt +500 -0
- package/assets/sudoku-hard.txt +500 -0
- package/assets/sudoku-kid.txt +500 -0
- package/assets/sudoku-medium.txt +500 -0
- 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/sudoku.js +92 -60
- 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
package/dist/commands/sudoku.js
CHANGED
|
@@ -1,74 +1,106 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from 'fs/promises';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { Jimp, JimpMime, loadFont, measureText } from 'jimp';
|
|
1
6
|
import { tryExecCommandFunction } from './util.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (let i = positions.length - 1; i > 0; i--) {
|
|
27
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
28
|
-
const tmp = positions[i];
|
|
29
|
-
positions[i] = positions[j];
|
|
30
|
-
positions[j] = tmp;
|
|
31
|
-
}
|
|
32
|
-
const grid = base.map(row => [...row]);
|
|
33
|
-
for (let i = 0; i < toRemove; i++) {
|
|
34
|
-
const pos = positions[i];
|
|
35
|
-
if (pos === undefined)
|
|
36
|
-
break;
|
|
37
|
-
const row = Math.floor(pos / 9);
|
|
38
|
-
const col = pos % 9;
|
|
39
|
-
if (grid[row])
|
|
40
|
-
grid[row][col] = 0;
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SUDOKU_DIR = join(__dirname, '../../assets');
|
|
9
|
+
const puzzleCache = new Map();
|
|
10
|
+
const FONT_DIR = new URL('../../node_modules/@jimp/plugin-print/dist/fonts/open-sans/', import.meta.url).pathname;
|
|
11
|
+
const SUDOKU_FONT = `${FONT_DIR}open-sans-32-black/open-sans-32-black.fnt`;
|
|
12
|
+
const IMAGE_WIDTH = 560;
|
|
13
|
+
const IMAGE_HEIGHT = 640;
|
|
14
|
+
const GRID_SIZE = 504;
|
|
15
|
+
const GRID_LEFT = (IMAGE_WIDTH - GRID_SIZE) / 2;
|
|
16
|
+
const GRID_TOP = 98;
|
|
17
|
+
const CELL_SIZE = GRID_SIZE / 9;
|
|
18
|
+
async function loadSudokus(difficulty) {
|
|
19
|
+
let puzzles = puzzleCache.get(difficulty);
|
|
20
|
+
if (!puzzles) {
|
|
21
|
+
puzzles = readFile(join(SUDOKU_DIR, `sudoku-${difficulty}.txt`), 'utf8').then(contents => {
|
|
22
|
+
const lines = contents.split(/\r?\n/).filter(Boolean);
|
|
23
|
+
return lines.map((puzzle, index) => {
|
|
24
|
+
if (puzzle.length !== 81 || /[^.1-9]/.test(puzzle)) {
|
|
25
|
+
throw new Error(`Invalid Sudoku puzzle at ${difficulty} asset line ${index + 1}`);
|
|
26
|
+
}
|
|
27
|
+
return Array.from({ length: 9 }, (_, row) => Array.from(puzzle.slice(row * 9, row * 9 + 9), cell => cell === '.' ? 0 : Number(cell)));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
puzzleCache.set(difficulty, puzzles);
|
|
41
31
|
}
|
|
42
|
-
return
|
|
32
|
+
return puzzles;
|
|
43
33
|
}
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
|
|
34
|
+
async function renderSudokuImage(grid, difficulty) {
|
|
35
|
+
const font = await loadFont(SUDOKU_FONT);
|
|
36
|
+
const image = new Jimp({ width: IMAGE_WIDTH, height: IMAGE_HEIGHT, color: 0xffffffff });
|
|
37
|
+
const title = `Sudoku - ${difficulty}`;
|
|
38
|
+
image.print({
|
|
39
|
+
font,
|
|
40
|
+
x: (IMAGE_WIDTH - measureText(font, title)) / 2,
|
|
41
|
+
y: 24,
|
|
42
|
+
text: title,
|
|
43
|
+
});
|
|
44
|
+
for (let row = 0; row < 10; row++) {
|
|
45
|
+
const thickness = row % 3 === 0 ? 4 : 1;
|
|
46
|
+
const y = Math.round(GRID_TOP + row * CELL_SIZE);
|
|
47
|
+
for (let offset = 0; offset < thickness; offset++) {
|
|
48
|
+
for (let x = GRID_LEFT; x <= GRID_LEFT + GRID_SIZE; x++) {
|
|
49
|
+
image.setPixelColor(0x000000ff, x, y + offset);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (let col = 0; col < 10; col++) {
|
|
54
|
+
const thickness = col % 3 === 0 ? 4 : 1;
|
|
55
|
+
const x = Math.round(GRID_LEFT + col * CELL_SIZE);
|
|
56
|
+
for (let offset = 0; offset < thickness; offset++) {
|
|
57
|
+
for (let y = GRID_TOP; y <= GRID_TOP + GRID_SIZE; y++) {
|
|
58
|
+
image.setPixelColor(0x000000ff, x + offset, y);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
for (let row = 0; row < 9; row++) {
|
|
48
|
-
const cells = grid[row]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
const cells = grid[row];
|
|
64
|
+
if (!cells)
|
|
65
|
+
throw new Error(`Missing Sudoku row ${row + 1}`);
|
|
66
|
+
for (let col = 0; col < 9; col++) {
|
|
67
|
+
const value = cells[col];
|
|
68
|
+
if (value === undefined || value === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const text = String(value);
|
|
71
|
+
const x = GRID_LEFT + col * CELL_SIZE + (CELL_SIZE - measureText(font, text)) / 2;
|
|
72
|
+
image.print({
|
|
73
|
+
font,
|
|
74
|
+
x,
|
|
75
|
+
y: GRID_TOP + row * CELL_SIZE + 9,
|
|
76
|
+
text,
|
|
77
|
+
});
|
|
55
78
|
}
|
|
56
79
|
}
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
const imagePath = join(tmpdir(), `windsor-sudoku-${process.pid}-${Date.now()}.png`);
|
|
81
|
+
await writeFile(imagePath, await image.getBuffer(JimpMime.png));
|
|
82
|
+
return imagePath;
|
|
59
83
|
}
|
|
60
84
|
async function printSudokuWorker(args, ctx) {
|
|
61
85
|
// !TODO: Implement good command parsing
|
|
62
86
|
const difficulty = args;
|
|
63
87
|
const diff = difficulty || 'easy';
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
const puzzles = await loadSudokus(diff);
|
|
89
|
+
const grid = puzzles[Math.floor(Math.random() * puzzles.length)];
|
|
90
|
+
if (!grid)
|
|
91
|
+
throw new Error(`No Sudoku puzzles available for ${diff}`);
|
|
92
|
+
const imagePath = await renderSudokuImage(grid, diff);
|
|
93
|
+
try {
|
|
94
|
+
const job = {
|
|
95
|
+
urls: [],
|
|
96
|
+
lines: [],
|
|
97
|
+
iconPath: imagePath,
|
|
98
|
+
};
|
|
99
|
+
await ctx.printJob(job);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await unlink(imagePath);
|
|
103
|
+
}
|
|
72
104
|
return {
|
|
73
105
|
kind: "pass"
|
|
74
106
|
};
|
|
@@ -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
|
};
|