windsor-bot 0.1.6 → 0.1.7
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/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/dist/commands/sudoku.js +92 -60
- package/package.json +1 -1
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
|
};
|