Node.js下完成洛谷冬日绘版

扩散性百万甜面包

2018-06-21 16:55:49

Tech. & Eng.

前言

Node.js 本身使用事件驱动、非阻塞和异步输入输出模型等技术,非常方便, 我们也可以将其利用成为冬日绘版中的佼佼者

本篇将简单介绍Node.js的使用,完成冬日绘版的自动提交

您可以需要了解:

什么是Node.js

白话版:

Node.js 可以让JavaScript代码跑在服务器上

专业版:

Node.js 通过 v8 (Chrome内核), 实现高效率的Web服务器, 最后实现出了单线程/单进程系统

实现思路

利用 Node.js 事件驱动的优势, 根据每次事件间隔进行 post

主体代码如下:

const EventEmitter = require('events')

const poster = new EventEmitter()

poster.on('start', () => { 
  post('luogu.org/', {})
})

setInterval(() => poster.emit('start'), 30 * 1000)

我们设定30的间隔秒然后进行post

然后我们思考几个细节

  1. 如何处理post失败情况

忽略即可, 失败的原因可能是你post太频繁了, 但是这并不影响冷却. 或者是因为洛谷更新导致需要refer字段等各种玄学问题

  1. 如何把我们的图片转化成洛谷所提供的格式

我们先预处理出图片内容

  1. 如何防止重复提交一个点多次

我们事件中加入一个检查画板的事件, 他定时帮助我们检查哪些是不需要的任务

  1. 如何让我代码更具扩展性, 比如我立马需要添加其他小伙伴的Cookie/图片数据

我们使用模块化, 将多个部分拆分开来, 最后组成完整的 Poster

预处理

我们使用 Python3, 使用第三方库 Pillow, 读取图片每个像素的颜色, 和洛谷提供的数据进行一一比对, 然后选择最适合的值

伪代码如下:

def main():
    img = ReadImg(imgPath) # 读取图片
    height, width = img.size()
    data = []

    for i in range(0, height):
      for j in range(0, width):
        color = getLuoguColor(img[i][j].color)
        data.append([i, j, color])

    data.save()

注意, 最小颜色差值也并不是我们直观上的欧几里得距离

有几篇文章阐述了原理, 有关详细原理超出编者的知识水平, 我这里不过多阐述, 供上链接

COLOR SPACE CONVERSION

Colour metric

我们直接使用Python自带库 colorsys

部分代码如下, 具体可以到我的项目中查看

map = {} # 这里是洛谷提供的颜色值
def get_color(pixel):
    return min_color_diff(pixel, colors)[1]
def to_hsv(color):
    return rgb_to_hsv(*[x / 255.0 for x in color])
def color_dist(c1, c2):
    return sum((a - b) ** 2 for a, b in zip(to_hsv(c1), to_hsv(c2)))
def min_color_diff(color_to_match, colors):
    return min(
        (color_dist(color_to_match, test), colors[test])
        for test in colors)

检查画板

洛谷给出了画板的链接 board

首先我们得知道的是哪个方向才是X/Y轴

多点几个点就能看出来了

然后我们发现里面有 a, b, c... 这样的内容, 洛谷为了方便起见10以上的分别用abc表示

我们将返回的字符串序列进行简单处理

function parseMap() {
  const _ = []
    map.trim().split('\n').forEach((v, k) => {
      _[k] = new Proxy({ value: v }, {
        get (target, p) {
          // 这里使用一个代理, 你可以理解为重载运算符, 将 map[x][y] 的操作进行转换
          // 避免 map[x][y] 返回字母, 而是要数字, 方便我们比对
          const val = target.value[p]
          return parseInt(val, 36) // 36进制足够用了, 省去手写转换的麻烦
        }
      })
    })
    return _
}

扩展性

我们通过继承 EventEmitter 实现其他功能

伪代码如下:

const EventEmitter = require('events')

class Poster extends EventEmitter {
  constructor (props) {
    super()
    this.tasks = loadTasks()
    this.users = loadUsers()
    this.map = getMap()

    this.registerEvent()
  }

  registerEvent () {
    this.on('checkMap', () => {
      this.tasks = reloadTask(this.tasks, this.map)
    })

    this.on('start', () => {
      for (const k in this.users) {
        const user = this.users[k]
        const cookie = user.cookie
        const data = this.tasks[0]
        this.tasks.shift()  // pop
        const [x, y, color] = data
        post('xxx', {
          x: x,
          y: y,
          color: color
        }, { cookie: cookie }).then(res => {
          if (res.data.status !== 200) {
            console.error('出错了!')
            this.tasks.push(data)  // 失败时候重新加入数组
            // other code
          } else {
            console.log('成功!')
            // other code
          }
        })
      }
    })
  }
}

const poster = new Poster()

setInterval(() => poster.emit('start'), 30 * 1000)
setInterval(() => poster.emit('checkMap'), 500 * 1000)  // 检查地图无需时间间隔太短

其他部分

结束

详细的代码在 Github luogu-drawer 中

其中 Poster 在 poster.js 内

鸣谢

yyfcpp 给了我很多Cookie, 以便我不到半小时就画完了我的头像

abc1763613206 帮助我维护服务器上的 luogu-drawer