Project

Nitendon Switch Tetris Bot

Switch Bot开发日记系列


近期进展:

  1. 重新实现Controller

  2. 新电路图

  3. 图像识别

  4. 游戏流程与AI&训练


上一篇日记提到

usb hub(adapter)到了,测试了一下非常给力,能够完美模拟手柄上的按键

于是第二天这玩意儿就坏了,尝试各种办法无法修复。看了下网上的评价也是用不到一周就坏了之类的。所以果断退货了(大家千万别买呀)。

于是只好思考另一种实现控制Switch的方法。既然有这种Adapter存在,为什么不可以自己做一个Adapter?

先从研究这个坏掉的Adapter入手,拆开看里面的结构也很简单,记得主控是一个STM32,pcb板上还是中文标记usb的”黑绿白红”之类的。不过STM32是基于ARM架构的,自己有的 Atmega32u4是基于AVR架构。首先寻找有没有Library可以实现模拟Switch的Driver。

Nintendo Switch Reverse Engineering 这个Repo是在探索的过程中非常有用的一个,作者把Joy-Cons反编译到的东西都放进里面,介绍了通过物理连接和蓝牙连接连接到Switch的原理图。

进一步的,有人提问道有没有获取Switch手柄通讯协议的方法 (传送门) 通过模拟手柄与Controller通讯的descriptor以及按键命令的字节码,我们可以为单片机加入蓝牙模块伪造一个Switch”原版”手柄。但是该反编译 还是不完整的,它无法提供一个可靠的解决方案。

直到我发现了这个repo

On June 20, 2017, Nintendo released System Update v3.0.0 for the Nintendo Switch. Along with a number of additional features that were advertised or noted in the changelog, additional hidden features were added. One of those features allows for the use of compatible USB controllers on the Nintendo Switch, such as the Pokken Tournament Pro Pad.

Unlike the Wii U, which handles these controllers on a ‘per-game’ basis, the Switch treats the Pokken controller as if it was a Switch Pro Controller. Along with having the icon for the Pro Controller, it functions just like it in terms of using it in other games, apart from the lack of physical controls such as analog sticks, the buttons for the stick clicks, or other system buttons such as Home or Capture.

The original version of the code that this repo is based off of emulated the Pokken Tournament Pro Pad, but changes have been made to support the HORIPAD wired controller for Nintendo Switch instead. In addition, many additional features/improvements have been added.

简单地来说,就是两年前Switch新固件开放了对旧手柄Pokken Tournament Pro Pad的支持,而这个repo就是以模拟Pokken Tournament Pro Pad的方式,添加了Switch特有的互动方式(增加的按键、摇杆等)。而作者 正是使用avr构架实现的模拟。

这样子,我们可以直接在此程序的基础上对我们的需求进行修改,将程序烧录值下位机,我觉得下位基于上位机通过更简单的pin input来通讯。


重新实现Controller过程

一开始上下位机都是使用Arduino IDE进行编程,所以对io的操作都非常方便,对源码进行修改后一键上传就ok了。 但要将程序烧录进单片机,就需要将源码make成hex文件再使用avrdude烧录。所以调试的时候还是遇到了些小麻烦。

首先是对io的操作不再是digitalWrite(1, HIGH)之类的了,为此还专门学习了一番 (传送门) 操作avr时使用到的端口reference是从avr/io.h中得到,而具体对应的端口需要查询芯片 引脚的文档 通过对DDR位运算控制output/input模式,通过对PORT控制output高低,对PIN读取input状态。

这是一个例子

    DDRB = 0b10001111;
    DDRC = 0b10111111;
    DDRD = 0b01101111;
    DDRE = 0b10111111;

将PB4-6、PC6、PD4和PD7、PE6设为input端口,其余(存在)的为output。

        // button 1
        if((PIND & (1<<PIND4))){
            PORTF |= (1<<PORTF7);
        }
        // button 2
        else if (PINC & (1<<PINC6)){
            PORTF &= ~(1<<PORTF7);
        }

如果PBD4为HIGH,则将PDF7设为HIGH(其余不变)。 反之,如果PBC6为HIGH,则将PDF7设为LOW(其余不变)。

通过这样的原理,可以直接对端口输入高低电压,从而使程序模拟不同的按键发送给Switch。 (发送按键部分)


新电路图

在完成上述的改变后,上下位机没必要再使用Serial通讯。直接使用NPN三极管作为控制开关就可以实现上位机对下位机的操控。所以电路图也做出了 更新

连接完毕后再也没有了操控时丢失指令、延迟过高等问题,没想到坏个Adapter是塞翁失马。


图像识别

终于可以开始最重要的部分了,这部分也是最令人头疼的一部分——发现了硬件上的问题都是可以看得见测得到的,而编程时会遇到很多抽象的问题,需要自己建立一个全新的模型去解决问题。

图像识别还是从日记#3的雏形入手,但经过了五六天,有了一些更好更新的想法。首先是寻找边缘坐标这一块发现OpenCV已经有了实现,所以#3中的 traceCoordinateInYDirection() 和 traceCoordinateInXDirection() 等方法已再没必要。

于是开始重新构建方块位置分析逻辑 (传送门,590行开始)

(虽然最后全部推倒重来了,因为有时候最简单的方法才是最有效的方法)

具体的逻辑大概是这样的:

(图一)黑点是点的坐标,不同颜色的线是不同的hierarchy,红点是分析到的Block,红框里的block是分析错的,绿框是没分析到的block。现在的算法是在过长的两点之间添加关键点,分析每个block四个顶点的坐标附近有没有黑点,如果四个点都有则就是结果,如果没分析到会进一步分析block内点的数量,大于一个阈值则也是结果,比如(图二)这几个。bug就出在这里,它也满足四个顶点的条件(图三蓝框),但并不应该是符合条件的block。而这几个缺少几个顶点(图四篮框)。图五图六是另外两个样本,红框是分析错的,篮框是没分析到的。这个顶点是cv的canny之后找到的contours,所以有层级关系。

图一

图二

图三

图四

图五

图六

正是上述遇到的问题,我又开始思考这种方式到底可不可行。如何用一种更快速有效的方法得到方块的位置信息。

突然恍然大悟——直接将图像转成grayscale,分析指定位置的明暗就可以了呀!设定一个颜色阈值,根据分析关键点(或者Gary老哥提到的方块中心点附近像素)的平均颜色,就可以判断有没有方块了。 这个原理既简单又有效,所以花了不到半个小时就将一下午所写的OpenCV代码替换掉了(忍痛注释掉500行) 之后又做了分析 接下来的方块 的部分。原理也很简单,根据一种方块起始坐标(比如I方块就是nextBlockIX, nextBlockIX),再根据预设好的判定方式去检测颜色就可以了,比如这个例子就是:

            boolean iFlag = true;
            for (int i = 0; i < 4; i++) {
                if (!(getBlue(image, nextBlockIX + i * nextBlockLength, nextBlockIY) > nextColorThreshold)) {
                    iFlag = false;
                }
            }
            if (iFlag) {
                result[0] = new BlockI();
                break;
            }

至此,经过了多重头脑风暴的图像分析部分算是搞定了。

游戏流程与AI&训练

游戏流程

已经获取到游戏界面后(下文称之为 Board ,包含了方块位置信息、下落方块、之后的方块等信息的Bean) 首先要做的就是模拟游戏基本的操作(旋转,移动,下落)。由于Board和方块(Block)的位置信息是以同样坐标系的matrix储存,对这些matrix进行分析就可以模拟方块下落的过程。 (见105:sumBoardAndBlockMatrix()方法)

有一个小问题就是下落中的方块会干扰程序的识别,所以暂时用removeFallingBlock方法从Board matrix中移除了下落中的方块位置信息,小问题就是最多只能分析到y=18时就无力了,之后优化该解决方案

AI部分

关于AI部分,刚开始写的其实不算是AI,只能算是识别器,因为训练不足。 程序在决定一个Block旋转多少次,在哪里下落前,会模拟所有的可能性。每一种可能性都会计算出一个分数,程序会选取最高分数的操作。

分数的实现 是由不同的参数,比重构成,刚开始这些参数都是我手动设置的。

比如消掉一行加五分,填充方块加一分,留空洞减三分,根据深度这些分数的权重也会变高

但是这些决定都是武断的,因为有时候留空更好,或者只留一个格叠很高等到有I方块后再消更有效率。所以必须要训练AI才能让它”聪明一点”。

训练部分

在此之前,score的计算方式稍做了改变 稍做了改变

如果方块上方留空则算做havingNoBlockAbove分数。

这个时候的训练还是很简单的模型,就是在已有的样本里(大概30多个不同已知board的instant),给出期待deltaX和rotation,尝试不同的计算score所需的参数。所得的平均成绩最高的即为最佳参数。

这种训练方式得到的结果的确比自选参数要好得多(一局里从总计最高消3-4行到可以最多消15行左右),但很难进一步的优化,因为参数设置等原因。

关于上述的训练方式,现在已经废弃掉了,取而代之的是Genetic Algorithm以及更好的score计算方式,这部分的实现仍然在构建中。