├── LICENSE ├── LuoguPaintBoard.gpl ├── LuoguPaintBoard.irs ├── README.md ├── cookies.json ├── data ├── ImageToData.py ├── board.json └── board2.json ├── paint.py ├── preview.html ├── runbattle.bat ├── runbattle.sh ├── runorder.bat ├── runorder.sh ├── runrand.bat └── runrand.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ouuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LuoguPaintBoard.gpl: -------------------------------------------------------------------------------- 1 | GIMP Palette 2 | Name LuoguPaintBoard 3 | # 4 | 255 255 255 5 | 170 170 170 6 | 85 85 85 7 | 254 211 199 8 | 255 196 206 9 | 250 172 142 10 | 255 139 131 11 | 244 67 54 12 | 233 30 99 13 | 226 102 158 14 | 156 39 176 15 | 103 58 183 16 | 63 81 181 17 | 0 70 112 18 | 5 113 151 19 | 33 150 243 20 | 0 188 212 21 | 59 229 219 22 | 151 253 220 23 | 22 115 0 24 | 55 169 60 25 | 137 230 66 26 | 215 255 7 27 | 255 246 209 28 | 248 203 140 29 | 255 235 59 30 | 255 193 7 31 | 255 152 0 32 | 255 87 34 33 | 184 63 39 34 | 121 85 72 35 | -------------------------------------------------------------------------------- /LuoguPaintBoard.irs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ouuan/LuoguPaintBoard/31c3f9ee550c10b88dcba6435b9b5e40804cf31f/LuoguPaintBoard.irs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 关于这个项目 2 | 3 | **本项目已停止维护**,大约明年会写一个新的。你可以继续使用,但作者**不会回答问题、修复 bug、更新功能**。 4 | 5 | 1. 这个项目是我两年前写的,当时我,非常菜 :new_moon_with_face: 6 | 2. 这个项目是同步(sync)的,所以,Cookies 数量比较多(我之前用的时候大约是超过 30~40 个)时,即使把两次绘制的间隔时间调到 0 也无法充分利用每个 Cookie(即,一个 Cookie 的冷却时间已经到了,但还没轮到它)。 7 | 3. 我想写一个异步的工具。 8 | 4. 紧接着,我想:为什么不写一个把绘制绘板的各个流程集成到一起的工具呢? 9 | 5. 我想用 Electron 写。 10 | 6. Electron 太难用了。 11 | 7. 我今年高三。 12 | 13 | 只不过我还是写了一个 [模拟洛谷冬日绘板服务器](https://github.com/ouuan/fake-luogu-paintboard-server)。 14 | 15 | ## 使用教程 16 | 17 | ### 准备环境 18 | 19 | - [python 3](https://www.python.org/downloads/) 20 | 21 | - [GIMP](https://www.gimp.org/downloads/) 或者 Photoshop 22 | 23 | - pillow:`python -m pip install pillow` 24 | 25 | - requests:`python -m pip install requests` 26 | 27 | ### 处理图像 28 | 29 | 于以下两项中选择一项: 30 | 31 | #### GIMP 32 | 33 | 1. 将 `LuoguPaintBoard.gpl` 复制到 `GIMP安装路径\share\gimp\2.0\palettes`。 34 | 35 | 2. 用 GIMP 打开原始图片。 36 | 37 | 3. 图像 → 缩放图像。 38 | 39 | 4. 图像 → 模式 → 索引,Use custom palette:LuoguPaintBoard,**不要** 勾选“从颜色表中移除无用和重复的颜色”。“递色”选项可以自己试试看哪种效果比较好。 40 | 41 | 5. 文件 → 导出为 → 选一个路径 → 选择文件类型:**bmp** → 导出。 42 | 43 | #### PS 44 | 45 | 1. 将`LuoguPaintBoard.irs` 复制到 `Photoshop安装路径\Presets\Optimized Settings`。 46 | 47 | 2. 用 Photoshop 打开原始图片。 48 | 49 | 3. 缩放图像。 50 | 51 | 4. Ctrl+Shift+Alt+S, 然后 Preset 选择 LuoguPaintBoard, Dither 自行选择。 52 | 53 | 5. 导出为 bmp 即可。 54 | 55 | ### 生成数据 56 | 57 | 请在 `data` 文件夹下生成数据。 58 | 59 | 1. `python ImageToData.py 图片路径 左上角X坐标 左上角Y坐标`,依次对每张图片运行(此脚本为增量更新)。 60 | 61 | 2. 打开 `preview.html` 检查数据生成是否有问题。 62 | 63 | ### 填写 cookies 64 | 65 | 在 `cookies.json` 里按如下格式填写: 66 | 67 | ``` 68 | [ 69 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx", 70 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx", 71 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx" 72 | ] 73 | ``` 74 | 75 | ### 运行脚本 76 | 77 | `python paint.py ` 78 | 79 | 三种模式分别为: 80 | 81 | - order: 每次画需要画的点中在 `board.json` 里最靠前的一个。 82 | 83 | - rand: 每次在需要画的点中随机选择。 84 | 85 | - battle: 优先画最近被修改过的格子。 86 | 87 | 为防止出现未被捕获的异常,建议循环运行脚本。 88 | 89 | ### 常量设置 90 | 91 | 时间的单位均为秒。 92 | 93 | - `WIDTH`:绘板宽度。 94 | - `HEIGHT`:绘板高度。 95 | - `COLORS`:颜色数量。 96 | - `GETTIMEOUT`:单次读取绘板(GET 请求)用时上限。 97 | - `POSTTIMEOUT`:单次画绘板(POST 请求)用时上限。 98 | - `GETBOARDFREQ`:读取绘板间隔时间。 99 | - `COOKIETIMEOUT`:一轮 Cookies 画完之后的等待时间。 100 | - `PAINTBOARDURL`:绘板地址。 101 | - `UA`:header 中使用的 User Agent。 102 | -------------------------------------------------------------------------------- /cookies.json: -------------------------------------------------------------------------------- 1 | [ 2 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx", 3 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx", 4 | "_uid=xxxxx;__client_id=xxxxxxxxxxxxx" 5 | ] -------------------------------------------------------------------------------- /data/ImageToData.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import json 3 | import sys 4 | 5 | DataPath = "board.json" 6 | Data2Path = "board2.json" 7 | 8 | if len(sys.argv) < 4: 9 | print("ImageToData.py ") 10 | quit() 11 | 12 | try: 13 | im = Image.open(sys.argv[1]) 14 | except: 15 | print('Open image ' + sys.argv[1] + ' failed.') 16 | quit() 17 | 18 | if im.mode != "P": 19 | print("Index image required. Please read README.md for more information.") 20 | quit() 21 | 22 | img_array = im.load() 23 | 24 | try: 25 | with open(DataPath, 'r') as boardjson: 26 | board = json.load(boardjson) 27 | except: 28 | board = [] 29 | 30 | w, h = im.size 31 | for i in range(w): 32 | for j in range(h): 33 | board.append([i + int(sys.argv[2]), j + int(sys.argv[3]), img_array[i, j]]) 34 | 35 | board = json.dumps(board) 36 | with open(DataPath, 'w+') as f: 37 | f.write(board) 38 | with open(Data2Path, 'w+') as f: 39 | f.write("var board = " + board) 40 | 41 | print("Finished.") 42 | -------------------------------------------------------------------------------- /data/board.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /data/board2.json: -------------------------------------------------------------------------------- 1 | var board = [] -------------------------------------------------------------------------------- /paint.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | import time 4 | import json 5 | import sys 6 | 7 | WIDTH = 800 8 | HEIGHT = 400 9 | COLORS = 32 10 | GETTIMEOUT = 5 11 | POSTTIMEOUT = 5 12 | GETBOARDFREQ = 20 13 | COOKIETIMEOUT = 10 14 | PAINTBOARDURL = "https://www.luogu.com.cn/paintBoard" 15 | UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" 16 | 17 | if len(sys.argv) == 1 or not(sys.argv[1] in ['order', 'rand', 'battle']): 18 | print('python paint.py ') 19 | quit() 20 | 21 | mode = sys.argv[1] 22 | 23 | with open('cookies.json', 'r') as cookiesjson: 24 | cookies = json.load(cookiesjson) 25 | 26 | if len(cookies) == 0: 27 | print('No cookie found.') 28 | quit() 29 | 30 | with open("data/board.json", 'r') as boardjson: 31 | board = json.load(boardjson) 32 | 33 | if len(board) == 0: 34 | print('The plan is empty.') 35 | quit() 36 | 37 | cur = 0 38 | cnt = 0 39 | 40 | now = [[2 for j in range(HEIGHT)] for i in range(WIDTH)] 41 | old = [[2 for j in range(HEIGHT)] for i in range(WIDTH)] 42 | lastChange = [[0 for j in range(HEIGHT)] for i in range(WIDTH)] 43 | 44 | def getCookie(): 45 | global cur, COOKIETIMEOUT 46 | cur = (cur + 1) % len(cookies) 47 | if cur == 0: 48 | time.sleep(COOKIETIMEOUT) 49 | return cookies[cur] 50 | 51 | def paint(x, y, col): 52 | global cur, PAINTBOARDURL, UA, POSTTIMEOUT 53 | data = { 54 | 'x': x, 55 | 'y': y, 56 | 'color': col 57 | } 58 | headers = { 59 | "refer": PAINTBOARDURL, 60 | "user-agent": UA, 61 | "cookie": getCookie() 62 | } 63 | try: 64 | response = requests.post(PAINTBOARDURL + "/paint", data = data, headers = headers, timeout = POSTTIMEOUT) 65 | except: 66 | print("POST error: ", cur) 67 | return -1 68 | try: 69 | if response.json()['status'] == 200: 70 | now[x][y] = col 71 | print(cur, x, y, col) 72 | return 0 73 | elif response.json()['status'] == 500: 74 | print(cur, response.json()['data']) 75 | return 1 76 | else: 77 | print(cur, response.json()['data']) 78 | return 2 79 | except: 80 | print(cur, response.text) 81 | return 3 82 | 83 | lastGet = 0 84 | 85 | def getBoard(): 86 | global cnt, lastGet, HEIGHT, WIDTH, COLORS, PAINTBOARDURL, GETTIMEOUT 87 | lastGet = time.time() 88 | try: 89 | getheader = { 90 | "refer": PAINTBOARDURL, 91 | "user-agent": UA 92 | } 93 | response = requests.get(PAINTBOARDURL + "/board", headers = getheader, timeout = GETTIMEOUT) 94 | except: 95 | print("Get board failed.") 96 | else: 97 | try: 98 | cnt = cnt + 1 99 | for i in range(WIDTH): 100 | for j in range(HEIGHT): 101 | old[i][j] = now[i][j] 102 | res = response.text 103 | for i in range(WIDTH): 104 | for j in range(HEIGHT): 105 | now[i][j] = int(res[i * (HEIGHT + 1) + j], COLORS) 106 | if now[i][j] != old[i][j]: 107 | lastChange[i][j] = cnt 108 | remain = 0 109 | for i in board: 110 | if now[i[0]][i[1]] != i[2]: 111 | remain = remain + 1 112 | print('Remain: ', remain) 113 | except: 114 | print('Get response parse error.') 115 | 116 | def chooseCell(): 117 | if mode == 'order': 118 | for cell in board: 119 | x = cell[0] 120 | y = cell[1] 121 | if now[x][y] != cell[2]: 122 | return cell 123 | return random.choice(board) 124 | elif mode == 'rand': 125 | todolist = [] 126 | for cell in board: 127 | x = cell[0] 128 | y = cell[1] 129 | if now[x][y] != cell[2]: 130 | todolist.append(cell) 131 | if len(todolist) == 0: 132 | return random.choice(board) 133 | return random.choice(todolist) 134 | elif mode == 'battle': 135 | mostRecentChanged = -1 136 | for cell in board: 137 | x = cell[0] 138 | y = cell[1] 139 | if now[x][y] != cell[2] and lastChange[x][y] > mostRecentChanged: 140 | mostRecentChanged = lastChange[x][y] 141 | if mostRecentChanged >= 0: 142 | print('Most recent changed: ' + str(mostRecentChanged)) 143 | else: 144 | return random.choice(board) 145 | todolist = [] 146 | for cell in board: 147 | x = cell[0] 148 | y = cell[1] 149 | if now[x][y] != cell[2] and lastChange[x][y] == mostRecentChanged: 150 | todolist.append(cell) 151 | return random.choice(todolist) 152 | 153 | getBoard() 154 | 155 | while True: 156 | if (time.time() - lastGet > GETBOARDFREQ): 157 | getBoard() 158 | cell = chooseCell() 159 | x = cell[0] 160 | y = cell[1] 161 | col = cell[2] 162 | if now[x][y] != col: 163 | while True: 164 | if paint(x, y, col) == 0: 165 | break 166 | elif time.time() - lastGet > 1: 167 | getBoard() 168 | -------------------------------------------------------------------------------- /preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 图片预览 6 | 7 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /runbattle.bat: -------------------------------------------------------------------------------- 1 | :loop 2 | python paint.py battle 3 | goto loop -------------------------------------------------------------------------------- /runbattle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while : 3 | do 4 | python3 paint.py battle 5 | done -------------------------------------------------------------------------------- /runorder.bat: -------------------------------------------------------------------------------- 1 | :loop 2 | python paint.py order 3 | goto loop -------------------------------------------------------------------------------- /runorder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while : 3 | do 4 | python3 paint.py order 5 | done -------------------------------------------------------------------------------- /runrand.bat: -------------------------------------------------------------------------------- 1 | :loop 2 | python paint.py rand 3 | goto loop -------------------------------------------------------------------------------- /runrand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while : 3 | do 4 | python3 paint.py rand 5 | done --------------------------------------------------------------------------------