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.
@@ -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 THEMES = [
3
- {
4
- name: 'Animals',
5
- words: ['CAT', 'DOG', 'BIRD', 'FISH', 'LION', 'BEAR', 'FROG', 'DUCK', 'WOLF', 'DEER'],
6
- },
7
- {
8
- name: 'Fruits',
9
- words: ['APPLE', 'MANGO', 'GRAPE', 'PEACH', 'PLUM', 'PEAR', 'LIME', 'KIWI', 'FIG', 'DATE'],
10
- },
11
- {
12
- name: 'Colors',
13
- words: ['RED', 'BLUE', 'GREEN', 'PINK', 'GOLD', 'TEAL', 'CORAL', 'TAN', 'IVORY', 'NAVY'],
14
- },
15
- {
16
- name: 'Space',
17
- words: ['STAR', 'MOON', 'SUN', 'MARS', 'EARTH', 'VENUS', 'COMET', 'ORBIT', 'NOVA', 'RING'],
18
- },
19
- {
20
- name: 'Food',
21
- words: ['PIZZA', 'TACO', 'SOUP', 'CAKE', 'RICE', 'BREAD', 'PASTA', 'SALAD', 'CHIP', 'PIE'],
22
- },
23
- ];
24
- const SIZE = 15;
25
- function makeGrid() {
26
- return Array.from({ length: SIZE }, () => Array(SIZE).fill(''));
27
- }
28
- const DIRECTIONS = [
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 (fits) {
53
- for (let i = 0; i < word.length; i++) {
54
- const r = row + dr * i;
55
- const c = col + dc * i;
56
- if (grid[r])
57
- grid[r][c] = word[i];
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
- return true;
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
- return false;
53
+ if (puzzles.length === 0)
54
+ throw new Error('No wordsearch assets found');
55
+ return puzzles;
64
56
  }
65
- const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
66
- function fillGrid(grid) {
67
- for (let r = 0; r < SIZE; r++) {
68
- for (let c = 0; c < SIZE; c++) {
69
- if (!grid[r]?.[c]) {
70
- if (grid[r])
71
- grid[r][c] = LETTERS[Math.floor(Math.random() * 26)] ?? 'A';
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
- function renderGrid(grid) {
77
- return grid.map(row => row.join(' '));
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
- const theme = THEMES[Math.floor(Math.random() * THEMES.length)];
81
- if (!theme)
82
- throw new Error('No themes available');
83
- const grid = makeGrid();
84
- const placed = [];
85
- for (const word of theme.words) {
86
- if (placeWord(grid, word))
87
- placed.push(word);
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
  };
package/dist/reactions.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const Reaction = {
2
2
  ok: "✅",
3
3
  what: "❓",
4
- fail: "❌"
4
+ fail: "❌",
5
+ thinking: "🧠"
5
6
  };