├── .editorconfig ├── .gitignore ├── README.md ├── app.js ├── config-example.json ├── package-lock.json ├── package.json ├── pictures └── .gitkeep └── scripts └── loadPic.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.json 3 | pictures/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luogu-Painter 2 | 3 | ## Usage 4 | 5 | 在洛谷「冬日绘版」中自动 Painting! 6 | 7 | ## Configuration 8 | 9 | 1. Clone 项目到本地。 10 | 2. `npm install`。 11 | 3. 使用 `scripts/loadPic.py` 生成图片的 `json` 格式,将其放到项目 `pictures` 目录下。 12 | - 需要安装 PIL 库:`pip install pillow`。 13 | 4. 复制 `config-example.json` 为 `config.json`,并配置以下内容: 14 | 1. `picFile`:生成好的图片,支持多张图同时绘制: 15 | - `name`:`json` 文件名。 16 | - `x`,`y`:绘制时的坐标偏移量。 17 | 2. `fetchTime`:更新地图的时间间隔,建议不要太小。(单位为 ms) 18 | 3. `paintTime`:每个用户每次 paint 的时间间隔,建议比洛谷限制稍大。(单位为 ms) 19 | 4. `random`:如果为 `true`,则每次随机选择需要绘制的点进行绘制;否则按「图片编号为第一关键字、坐标顺序为第二关键字」排序然后绘制。 20 | 5. `users`:绘制所用的用户 `token`,可添加多个。 21 | 5. `npm start`,开始你的创作! 22 | 23 | ## Thanks 24 | 25 | - Luogu-Painter 使用的 `scripts/loadPic.py` 是 [AimonaStudio/luogu-drawer](https://github.com/AimonaStudio/luogu-drawer/blob/master/scripts/main.py) 里的脚本的修改版,非常感谢! 26 | - 感谢为 Luogu-Painter 提供测试的 [ouuan/fake-luogu-paintboard-server](https://github.com/ouuan/fake-luogu-paintboard-server)! 27 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const fetch = require('node-fetch'); 6 | const querystring = require('querystring'); 7 | const process = require('process'); 8 | 9 | const luoguPaintBoardUrl = 'https://www.luogu.com.cn/paintboard'; 10 | 11 | let config; 12 | let pic = []; 13 | let board = [], lastGetBoardTime, reqPaintPos = []; 14 | 15 | main(); 16 | 17 | async function main() { 18 | console.log('app.js Being loaded...'); 19 | getConfig(); 20 | getPic(); 21 | await getBoard(); 22 | 23 | while (true) { 24 | if (Date.now() - lastGetBoardTime >= config.fetchTime) { 25 | await getBoard(); 26 | } 27 | for (let user of config.users) { 28 | if (Date.now() - user.lastPaintTime < config.paintTime) { 29 | continue; 30 | } 31 | if (reqPaintPos.length) { 32 | user.lastPaintTime = Date.now(); 33 | let data = reqPaintPos.shift(); 34 | if (!await paintBoard(user, data)) { 35 | reqPaintPos.push(data); 36 | } 37 | break; 38 | } 39 | } 40 | } 41 | } 42 | 43 | function getConfig() { 44 | try { 45 | config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8')); 46 | for (let user of config.users) { 47 | user.lastPaintTime = Date.now() - config.lastPaintTime; 48 | } 49 | } catch (err) { 50 | console.error('Get config.json Failed.'); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | function getPic() { 56 | try { 57 | for (let p of config.picFile) { 58 | pic.push({ 59 | x: p.x, 60 | y: p.y, 61 | map: JSON.parse(fs.readFileSync(path.join(__dirname, 'pictures', p.name), 'utf-8')) 62 | }); 63 | } 64 | } catch (err) { 65 | console.error('Get Pictures Failed.'); 66 | process.exit(1); 67 | } 68 | } 69 | 70 | async function getBoard() { 71 | lastGetBoardTime = Date.now(); 72 | try { 73 | let str = await fetch(luoguPaintBoardUrl + '/board'); 74 | board = (await str.text()).split('\n'); 75 | if (!board[board.length - 1]) { 76 | board.pop(); 77 | } 78 | console.log(new Date().toLocaleString(), 'Get PaintBoard Succeeded.'); 79 | getReqPaintPos(); 80 | } catch (err) { 81 | console.warn(new Date().toLocaleString(), 'Get PaintBoard Failed:', err); 82 | } 83 | } 84 | 85 | function getReqPaintPos() { 86 | try { 87 | reqPaintPos = []; 88 | for (let p of pic) { 89 | for (let pix of p.map) { 90 | if (parseInt(board[pix.x + p.x][pix.y + p.y], 36) != pix.color) { 91 | reqPaintPos.push({ 92 | x: pix.x + p.x, 93 | y: pix.y + p.y, 94 | color: pix.color 95 | }); 96 | } 97 | } 98 | } 99 | if (config.random) { 100 | reqPaintPos.sort((a, b) => { return Math.random() - 0.5; }); 101 | } 102 | console.log(new Date().toLocaleString(), `Load reqPaintPos Succeeded: Size = ${reqPaintPos.length}.`); 103 | } catch (err) { 104 | console.warn(new Date().toLocaleString(), 'Load reqPaintPos Failed:', err); 105 | } 106 | } 107 | 108 | async function paintBoard(user, data) { 109 | try { 110 | let res = await fetch(`${luoguPaintBoardUrl}/paint?token=${user.token}`, { 111 | method: 'POST', 112 | headers: { 113 | 'content-type': 'application/x-www-form-urlencoded', 114 | 'referer': luoguPaintBoardUrl 115 | }, 116 | body: querystring.stringify(data) 117 | }); 118 | if (res.status == 200) { 119 | console.log(new Date().toLocaleString(), 'Paint PaintBoard Succeeded:', user.token, data); 120 | } else { 121 | throw new Error(JSON.stringify(await res.json())); 122 | } 123 | } catch (err) { 124 | console.warn(new Date().toLocaleString(), 'Paint PaintBoard Failed:', user.token, err.message); 125 | return false; 126 | } 127 | return true; 128 | } 129 | -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "picFile": [ 3 | { 4 | "name": "pic1.json", 5 | "x": 0, 6 | "y": 0 7 | }, 8 | { 9 | "name": "pic2.json", 10 | "x": 100, 11 | "y": 0 12 | } 13 | ], 14 | "fetchTime": 10000, 15 | "paintTime": 31000, 16 | "random": false, 17 | "users": [ 18 | { 19 | "token": "998244353:qaq" 20 | }, 21 | { 22 | "token": "998244853:qwq" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luogu-painter", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "luogu-painter", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "node-fetch": "^2.6.0" 13 | } 14 | }, 15 | "node_modules/node-fetch": { 16 | "version": "2.6.1", 17 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 18 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 19 | "engines": { 20 | "node": "4.x || >=6.0.0" 21 | } 22 | } 23 | }, 24 | "dependencies": { 25 | "node-fetch": { 26 | "version": "2.6.1", 27 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 28 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luogu-painter", 3 | "version": "1.0.0", 4 | "description": "The Painter of Luogu PaintBoard", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Early0v0/Luogu-Painter.git" 12 | }, 13 | "author": "Early ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/Early0v0/Luogu-Painter/issues" 17 | }, 18 | "homepage": "https://github.com/Early0v0/Luogu-Painter#readme", 19 | "dependencies": { 20 | "node-fetch": "^2.6.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pictures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Early0v0/Luogu-Painter/d55c05f46427e91ed5cfb0731ef37eed266a0730/pictures/.gitkeep -------------------------------------------------------------------------------- /scripts/loadPic.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import json 3 | 4 | imagePath = "./pic.png" 5 | 6 | # 文件地址,支持 .jpg 与 .png 格式。 7 | # 在 png 格式下不会维护纯透明像素,且会取消所有像素的透明度。建议将图片二元透明化处理后采用此模式。 8 | 9 | dataPath = '../pic.json' # 输出地址 10 | 11 | # 缩放后像素大小 12 | width = 48 13 | height = 48 14 | 15 | colors = { 16 | (0, 0, 0): 0, 17 | (255, 255, 255): 1, 18 | (170, 170, 170): 2, 19 | (85, 85, 85): 3, 20 | (254, 211, 199): 4, 21 | (255, 196, 206): 5, 22 | (250, 172, 142): 6, 23 | (255, 139, 131): 7, 24 | (244, 67, 54): 8, 25 | (233, 30, 99): 9, 26 | (226, 102, 158): 10, 27 | (156, 39, 176): 11, 28 | (103, 58, 183): 12, 29 | (63, 81, 181): 13, 30 | (0, 70, 112): 14, 31 | (5, 113, 151): 15, 32 | (33, 150, 243): 16, 33 | (0, 188, 212): 17, 34 | (59, 229, 219): 18, 35 | (151, 253, 220): 19, 36 | (22, 115, 0): 20, 37 | (55, 169, 60): 21, 38 | (137, 230, 66): 22, 39 | (215, 255, 7): 23, 40 | (255, 246, 209): 24, 41 | (248, 203, 140): 25, 42 | (255, 235, 59): 26, 43 | (255, 193, 7): 27, 44 | (255, 152, 0): 28, 45 | (255, 87, 34): 29, 46 | (184, 63, 39): 30, 47 | (121, 85, 72): 31, 48 | } 49 | 50 | 51 | def get_color(pixel): 52 | return min_color_diff(pixel, colors)[1] 53 | 54 | 55 | def color_dist(c1, c2): 56 | return sum(abs(a - b) for a, b in zip(c1, c2)) 57 | 58 | 59 | def min_color_diff(color_to_match, colors): 60 | return min( 61 | (color_dist(color_to_match, test), colors[test]) 62 | for test in colors) 63 | 64 | 65 | def save(data): 66 | data = json.dumps(data) 67 | with open(dataPath, 'w+') as f: 68 | f.write(data) 69 | print("Finished.") 70 | 71 | 72 | def main(): 73 | im = Image.open(imagePath) 74 | im = im.resize((width, height)) 75 | data = [] 76 | w, h = im.size 77 | print("width", w, "height", h) 78 | 79 | for i in range(0, w, 1): 80 | for j in range(0, h, 1): 81 | if im.mode == 'RGBA' and im.getpixel((i, j)) == (0, 0, 0, 0): continue 82 | color = get_color(im.getpixel((i, j))[0:2]) 83 | data.append({'x': i,'y': j,'color': color}) 84 | save(data) 85 | 86 | 87 | if __name__ == '__main__': 88 | main() 89 | --------------------------------------------------------------------------------