基于 Arduino 与灯带的贪吃蛇游戏
大一定下的的创客项目以及 Dev Feast 活动上展示内容,主要技术栈在短学期已搞定,关键在于贪吃蛇游戏的实现。
项目地址:https://github.com/SRayJay/snake
一、项目简介
本项目开发一个运行在 ESP32 上,显示在 WS2812 灯板上并收 flutterAPP 控制的贪吃蛇游戏。
二、项目准备
所需的材料:ESP32 一个,22*22 的 WS2812 灯屏、电源、亚克力板
所做的准备:MQTT 服务器、arduino 开发环境、FastLED 库,flutter 开发框架。
三、灯带的使用
FastLED 灯带库
项目采用 FastLED 库来控制灯带。
首先引入头文件,并作一些定义。
#include<FastLED.h> //引入头文件
#define PIN 22 //esp32上连接灯带的引脚
#define MAXLED 484 //最大灯数
CRGB leds[MAXLED]; //定义了rgb灯珠数组
然后在 setup 函数内添加一些内容
void setup(){
Serial.begin(115200);
FastLED.setBrightness(64);
FastLED.addLeds<WS2812,PIN,GRB>(leds,MAXLED);
}
灯带亮灭的用法
leds[0] = CRGB(255,0,0);
leds[1] = CRGB(0,255,0);
FastLED.show();
delay(80);
以上代码要求第一盏灯(数组从 0 开始)显示红色,第二盏灯显示绿色。前两句只是绑定颜色信息在灯珠上,真正产生效果的在 FastLED.show()这一句,delay(80)是 80 毫秒的延迟。
四、MQTT 服务器与 wifi 连接的准备
WIFI 的连接
#include<WiFi.h>//先导入库
const char* ssid = "xxxx";//WiFi的ssid和密码可以通过常量定义
const char* password = "xxxx";
void setup(){
Serial.begin(115200);
setup_wifi();//初始化函数调用启动wifi函数
}
void setup_wifi(){
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid,password);
while(WiFi.status()!=WL_CONNECTED){
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");//连接成功后显示IP信息
Serial.println("IP address:");
Serial.println(WiFi.localIP());
}
MQTT 服务器的连接
首先得准备一台可订阅发布消息的 mqtt 服务器,准备好后通过它的地址来连接。(这里省去了 mqtt 服务器用户信息,是不够安全的,可以自行设置用户名与密码信息。
#include<PubSubClient.h>
const char* mqtt_server = "xxxx";
WiFiClient espClient;
PubSubClient client(espClient);
void setup(){
client.setServer(mqtt_server,1883);
client.setCallback(callback);//设置回调函数,每次接收到消息都执行callback函数
}
void reconnect(){
while(!client.connected()){
Serial.print("Attempting MQTT connection...");
if(client.connect("ESP32Client")){
Serial.println("connected");
client.subscribe("mode");//订阅一个名为mode的主题
client.subscribe("control");//订阅control主题
}else{
Serial.print("failed,rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void loop() {
if(!client.connected()){//客户端连接断开后执行重连
reconnect();
}
client.loop();//连接成功后反复执行的循环函数,当它开始循环客户端才开始工作。
}
回调函数
每次回调执行的内容,传入的参数分别是主题、消息内容、长度。这里使用字符串比较函数 strcmp(str1,str2)来判断新传入的消息的主题是哪一个,这个函数返回的是一个整形数值,当它为 0 的时候表示两个字符串相同。
void callback(char* topic,byte* payload,unsigned int length){
if(strcmp(topic,"mode")==0){
opt = (char)payload[0];
Serial.println(opt);
}else if(strcmp(topic,"control")==0){
towards = (char)payload[0];
Serial.println(towards);
}
}
五、贪吃蛇基本原理
1、地图的初始化
地图是 22*22 的矩形,可看成一个二维数组 maps[22][22],内容为该点的状态信息,在这个项目里,一个点可能有四种情况,我们设置 0 代表该点为空,1 代表该点是墙体,2 代表该点是食物,3 代表该点是蛇身。对于没有墙体的最简单的模式,可以在初始化函数里将所有点初始化为 0。
2、地图与灯珠信息的转换
地图是 22*22 的二维数组,而灯带是 484 个长度的一维数组,在改变了地图信息后,需要相应的对灯带信息作出改变。而 ws2812 矩形灯板走的是单总线,是不断反转迂回的,所以第 21 盏灯和第 22 盏灯在不同行但是位置是上下邻接的。奇数行时灯带位置从左到右递增,偶数行则是从右到左递增。可以根据这一特点来写一段转换代码,或者将其封装为函数,在每次需要做出改变时调用。
for(i=0;i<MAXLED;i++){
if((i/22)%2==0){
if(maps[i/22][i%22] == 1){
leds[i] = CRGB(0,0,255);
}
}else{
if(maps[i/22][21-i%22] == 1){
leds[i] = CRGB(0,0,255);
}
}
}
//这段代码遍历所有灯,当灯的位置在奇数行时是从左到右递增,偶数行反之,转换的结果将地图数组为1的点设置为墙体,亮蓝光。
3、食物的结构
食物的位置需要一个坐标信息,可以采用一维坐标也可以采用二维坐标,但二维坐标显然更简洁、明了,于是全局定义 food 结构,包含 xy 坐标。并写出食物生成函数。
struct Food{
int x;
int y;
}food;
void create_food(){
int i,j;
do{
i = random(22);
j = random(22);
}while(maps[i][j]!=0);
food.y = i;
food.x = j;
maps[i][j]=2;
if(i%2==1){
int pos;
pos=(i+1)*22-1-j;
leds[pos]=CRGB(0,255,0);
}else{
leds[i*22+j]=CRGB(0,255,0);
}
}
这段代码实现了生成食物的随机性,直到随机出一个空的点来生成食物,在 maps 数组注册信息,并转换为一维坐标将该点对应的灯点亮为绿色。
4、蛇身结构
因为蛇身总是连着的,身体各部分相对位置是不变的 ,每次移动时观察头部和尾部,头部和尾部都会朝着当前的方向移动一格。用灯的视角来看,就是头部的前一方向在移动后亮起,而尾部则熄灭,这样的过程可以模拟蛇的移动。根据这些特点,我们采用链表的结构来定义蛇身。
typedef struct Snakes{
int x;
int y;
struct Snakes *next;
}snake;
snake *head;
//游戏开始时执行create_snake来初始化蛇
//坐标参数可根据设定的障碍物来设置
void create_snake(){
head = (snake*)malloc(sizeof(snake));
head->x = 12;
head->y = 10;
snake *p = (snake*)malloc(sizeof(snake));
snake *q = (snake*)malloc(sizeof(snake));
p->x = 11;
p->y = 10;
q->x = 10;
q->y = 10;
head->next = p;
p->next = q;
q->next = NULL;
//在地图数组上注册这些位置为蛇身
maps[head->y][head->x]=3;
maps[p->y][p->x]=3;
maps[q->y][q->x]=3;
//蛇身位置的灯将显示红色
leds[(head->y)*22+head->x]=CRGB(255,0,0);
leds[(p->y)*22+p->x]=CRGB(255,0,0);
leds[(q->y)*22+q->y]=CRGB(255,0,0);
}
5、蛇的移动
移动的过程这里采用了尾部先消失,再出现新头部的方法。新头部的位置需要判断是否为食物,若为食物,头部由食物的绿色变为红色,尾部也需要重新点亮,表示增加一个长度。
char towards = 'D';
//towards是一个全部变量,表示蛇的移动方向,默认为'D',也可以在不同关卡修改默认值
void snake_moving(){
int x = head->x, y = head->y;
snake *p = head->next;
//先默认关闭最后一盏灯
while(1){
if(p->next == NULL){
turn_down(p->y,p->x);
break;
}
p = p->next;
}
switch(towards){
case 'W':
if(head->y==0){
head->y = 21;
}else{
head->y -= 1;
}
judge();
//judge函数用于判断是否咬到墙或者蛇身来结束游戏
Serial.println("向上一步走");
break;
case 'A':
if(head->x == 0){
head->x = 21;
}else{
head->x -= 1;
}
judge();
Serial.println("向左一步走");
break;
case 'S':
if(head->y == 21){
head->y = 0;
}else{
head->y += 1;
}
judge();
Serial.println("向下一步走");
break;
case 'D':
if(head->x == 21){
head->x = 0;
}else{
head->x += 1;
}
judge();
Serial.println("向右一步走");
break;
}
ChangeBody(y,x);
}
void ChangeBody(int y,int x){
snake *p = head->next;
int pos_a,pos_b,pos_x,pos_y;
pos_a = x;
pos_b = y;
while(p!=NULL){
pos_x = p->x;
pos_y = p->y;
p->x = pos_a;
p->y = pos_b;
pos_a = pos_x;
pos_b = pos_y;
p = p->next;
}
if(head->x == food.x&&head->y == food.y){
snake *_new = (snake*)malloc(sizeof(snake));
p = head;
while(p->next!=NULL){
p = p->next;
}
_new->x = pos_a;
_new->y = pos_b;
p->next = _new;
_new ->next = NULL;
turn_on_body(head->y,head->x);
turn_on_body(_new->y,_new->x);
//吃到食物时,蛇头由食物的绿变红,蛇尾需要重新点亮
Serial.println("吃到食物");
snake_len++;
//吃到食物后再随机生成新的食物
create_food();
}else{
turn_on_body(head->y,head->x);
//没吃到食物,根据正常行进,前面已经turn_down蛇尾,这会儿点亮蛇头就行
}
//turn_on_body和turn_down两个函数只改变点的颜色与坐标信息,还需要外面调用show函数来刷新
FastLED.show();
}
void turn_on_body(int y,int x){
int pos;
pos = (y + 1) * 22 - 1 - x;
if(y % 2 ==1){
leds[pos] = CRGB(255,0,0);
}else{
leds[y * 22 + x] = CRGB(255,0,0);
}
maps[y][x] = 3;
}
void turn_down(int y,int x){
int pos;
pos = (y + 1) * 22 - 1 - x;
if(y%2==1){
leds[pos] = CRGB(0,0,0);
}else{
leds[y*22+x] = CRGB(0,0,0);
}
maps[y][x] = 0;
}
6、模式的选择
程序刚启动时是一条小蛇不断地移动作为等待界面,这个时候需要你给出一个模式的指令来开始游戏。
void welcome(){
leds[0] = CRGB(255,0,0);
leds[1] = CRGB(255,0,0);
for(int i=2;i<483;i++){
client.loop();
//这句表示它时刻接收传入数据,若opt改变,就终止等待界面,开始游戏
if(opt!='0'){
break;
}
leds[i] = CRGB(255,0,0);
leds[i+1] = CRGB(255,0,0);
leds[i-2] = CRGB(0,0,0);
FastLED.show();
delay(80);
}
}
char opt='0';//模式值默认为'0',等待界面
void loop() {
if(!client.connected()){
reconnect();
}
client.loop();
if(opt == '0'){
welcome();
}else if(opt == '2'){
mode2();
//四面都是墙
}else if(opt == '1'){
infinity_mode();
//无墙
}else if(opt == '3'){
mode3();
//四个L字墙
}else if(opt == '4'){
mode4();
}else if(opt == '5'){
mode5();
}else if(opt == '6'){
mode6();
}else if(opt == '7'){
mode7();
}else if(opt == '8'){
mode8();
}else if(opt == '9'){
mode9();
}else if(opt == 'B'){
battle_mode();
//闯关模式,吃十五个食物进入下一关
}
}
上面代码涉及到的其他的 mode,都可以自行设计,下面给出一个例子。
void mode5(){
clear_all();//清空地图的信息
create_wall5();//初始化关卡5的墙体
create_food();
create_snake();
FastLED.show();
delay(1000);
client.publish("snake_len","3");
//这里发布了主题为snake_len的消息,将蛇身长度传到app,只需要app接收这个主题的消息即可。
while(opt == '5'){
client.loop();
check_mode('5');//该函数检查有没有改变模式
snake_moving();
delay_time();//根据蛇身长度区间来加快蛇移动速度的函数
}
}
void clear_all(){
for(int i = 0;i<484;i++){
leds[i] = CRGB(0,0,0);
}
for(int i = 0;i<22;i++){
for(int j = 0;j<22;j++){
maps[i][j] = 0;
}
}
snake_len = 3;
score = 0;
itoa(score,score_msg,10);
client.publish("score",score_msg);
//这里发布的score主题的消息,表示分数,传给app
}
void create_wall5(){
int i,j;
for(j=0;j<10;j++){
maps[9][j] = 1;
}
for(i=0;i<9;i++){
maps[i][9] = 1;
}
for(j=16;j<22;j++){
maps[5][j] = 1;
maps[13][j] =1;
}
for(i=6;i<13;i++){
maps[i][16] = 1;
}
for(i=16;i<22;i++){
maps[i][6] = 1;
maps[i][17] = 1;
}
for(j=6;j<18;j++){
maps[16][j] = 1;
}
for(i=0;i<MAXLED;i++){
if((i/22)%2==0){
if(maps[i/22][i%22] == 1){
leds[i] = CRGB(0,0,255);
}
}else{
if(maps[i/22][21-i%22] == 1){
leds[i] = CRGB(0,0,255);
}
}
}
}
void check_mode(char now_mode){
if(opt!=now_mode){
if(opt == '0'){
welcome();
}else if(opt == '2'){
mode2();
//四面都是墙
}else if(opt == '1'){
infinity_mode();
//无墙
}else if(opt == '3'){
mode3();
//四个L字墙
}else if(opt == '4'){
mode4();
}else if(opt == '5'){
mode5();
}else if(opt == '6'){
mode6();
}else if(opt == '7'){
mode7();
}else if(opt == '8'){
mode8();
}else if(opt == '9'){
mode9();
}else if(opt == 'B'){
battle_mode();
//闯关模式,吃十五个食物进入下一关
}
}
}
void delay_time(){
if(snake_len<=10){
delay(475);
}else if(snake_len<=15){
delay(375);
}else{
delay(275);
}
}
贪吃蛇的核心代码到这里就差不多了,剩下的都是自行设计,详细代码可参考项目。
六、Flutter 控制器
1. mqtt 依赖
首先新建一个项目,在 pubspec.yaml 文件 dependencies 中加入 mqtt 应用依赖
mqtt_client: ^5.5.3
然后点击右上角 Package get 获取相关的依赖.
完成之后在 lib 目录下新建一个 package,并且新建一个 dart 文件 message.dart,在里面黏上下面的代码:
import 'package:mqtt_client/mqtt_client.dart' as mqtt;
class Message {
final String topic;
final String message;
final mqtt.MqttQos qos;
Message({this.topic, this.message, this.qos});
}
这个是一个 mqtt 消息的类,里面有一个消息的主题、内容和 Qos。
2、mqtt 相关代码
void _connect() async{
//client连接的初始化配置
//默认端口1883,如果不是1883就采用
//client = mqtt.MqttClient.withPort(broker, '',1883);
client = mqtt.MqttClient(broker,'');
client.logging(on: true);
client.keepAlivePeriod = 30;
client.onDisconnected = _onDisconnected;
final mqtt.MqttConnectMessage connMess = mqtt.MqttConnectMessage()
.withClientIdentifier('*******')//连接的id
.startClean()
.keepAliveFor(30)
.withWillTopic('willtopic')
.withWillMessage('My Will message')
.withWillQos(mqtt.MqttQos.atLeastOnce);
print('MQTT client connecting....');
client.connectionMessage = connMess;
try {
await client.connect();
} catch (e) {
print(e);
_disconnect();
}
if (client.connectionState == mqtt.MqttConnectionState.connected) {
print('MQTT client connected');
setState(() {
connectionState = client.connectionState;
});
} else {
print('ERROR: MQTT client connection failed - '
'disconnecting, state is ${client.connectionState}');
_disconnect();
}
subscription = client.updates.listen(_onMessage);
}
void _disconnect() {
client.disconnect();
_onDisconnected();
}
void _onDisconnected() {
setState(() {
connectionState = client.connectionState;
client = null;
});
print('MQTT client disconnected');
}
void _onMessage(List<mqtt.MqttReceivedMessage> event) {
print(event.length);
print(event[0].topic);
final mqtt.MqttPublishMessage recMess = event[0].payload as mqtt.MqttPublishMessage;
final String message = mqtt.MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
print('MQTT message: topic is <${event[0].topic}>, '
'payload is <-- ${message} -->');
print(client.connectionState);
setState(() {
//在这个地方判断接收的消息的主题并接收消息
if(event[0].topic=='snake_len'){
snake_len = int.parse(message);
print(snake_len);
}else if(event[0].topic=='score'){
score = int.parse(message);
print(score);
}
});
}
void _subMessage(){
//开始接收subtopic的submessage
print("on sub message");
if(connectionState == mqtt.MqttConnectionState.connected){
setState(() {
print('subscribe to ${_subSnakeLen}');
client.subscribe(_subSnakeLen, mqtt.MqttQos.exactlyOnce);
client.subscribe(_subScore, mqtt.MqttQos.exactlyOnce);
});
}
}
void _pubMessage(){
//发布控制消息
final mqtt.MqttClientPayloadBuilder builder =
mqtt.MqttClientPayloadBuilder();
builder.addString(_pubMsg);
print("pub message ${_pubControl}:${_pubMsg}");
client.publishMessage(
_pubControl,
mqtt.MqttQos.values[0],
builder.payload,
retain: _retainValue,
);
}
void _pubMode(){
//发布控制消息
final mqtt.MqttClientPayloadBuilder builder =
mqtt.MqttClientPayloadBuilder();
builder.addString(_pubMsg);
print("pub message ${_pubModeTopic}:${_pubMsg}");
client.publishMessage(
_pubModeTopic,
mqtt.MqttQos.values[0],
builder.payload,
retain: _retainValue,
);
}
3、控制器界面
由于 app 控制器比较简洁,只有一个界面,不需要路由导航也不需要菜单栏,
import 'package:flutter/material.dart';
import 'package:mqtt_client/mqtt_client.dart' as mqtt;
import 'model/message.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'snake',
home: MyHomePage(title: 'snake'),
);
}
}
class MyHomePage extends StatefulWidget{
MyHomePage({Key key,this.title}) : super(key:key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>{
int snake_len = 3;
int score = 0;
String _pubControl = 'control';
String _pubModeTopic = 'mode';
String _subSnakeLen = 'snake_len';
String _subScore = 'score';
String _pubMsg;
bool _retainValue = false;
ScrollController subMsgScrollController = new ScrollController();
String broker = '*****';//mqtt服务器地址
mqtt.MqttClient client;
mqtt.MqttConnectionState connectionState;
StreamSubscription subscription;
List<Message> messages = <Message>[];
@override
void initState(){
super.initState();
_connect();
_subMessage();
}
//_FreeModeOption是一个弹窗界面,在按下自由模式时就会显示这个弹窗来选择
//3*3的九个按钮,分别对应一种模式,在按下时绑定的消息就会发出,传达给esp32
_FreeModeOption(){
showDialog(
context: context,
builder: (BuildContext context){
return new SimpleDialog(
title: new Text('自由模式'),
children: <Widget>[
new Row(
children: <Widget>[
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '1';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式一'),
),
margin: EdgeInsets.fromLTRB(10,0,0,0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '2';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式二'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '3';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式三'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
],
),
new Row(
children: <Widget>[
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '4';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式四'),
),
margin: EdgeInsets.fromLTRB(10,0,0,0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '5';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式五'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '6';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式六'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
],
),
new Row(
children: <Widget>[
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '7';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式七'),
),
margin: EdgeInsets.fromLTRB(10,0,0,0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '8';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式八'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
new Container(
child: new OutlineButton(
onPressed: (){
_pubMsg = '9';
_pubMode();
score = 0;
_subMessage();
},
child: Text('模式九'),
),
margin: EdgeInsets.fromLTRB(10, 0, 0, 0),
),
],
)],
);
}
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: Text(widget.title),),
body: new Column(
children: <Widget>[
new Row(
children: <Widget>[
new Container(
width:107,
padding: EdgeInsets.fromLTRB(10, 10, 0, 0),
child:Text(
"蛇长:"+
snake_len.toString()
),
),
new Container(
padding: EdgeInsets.fromLTRB(180, 0, 0, 0),
child: RaisedButton(
child: Text('冒险模式'),
onPressed: (){
_pubMsg = 'B';
_pubMode();
score = 0;
_subMessage();
},
textColor: Colors.white,
color: Colors.deepOrangeAccent,
),
),
],
),
new Row(
children: <Widget>[
new Container(
width: 107,
padding: EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Text(
"得分:"+
score.toString()
),
),
new Container(
margin: EdgeInsets.fromLTRB(180, 0, 0, 0),
child: RaisedButton(
child: new Text('自由模式'),
onPressed: _FreeModeOption,
textColor: Colors.white,
color: Colors.deepOrangeAccent,
),
)
],
),
//方向控制分为三行
new Row(
children: <Widget>[
new Container(
padding: EdgeInsets.fromLTRB(175, 200, 0, 10),
child: FloatingActionButton(
child: new Icon(Icons.arrow_drop_up),
onPressed: (){
_pubMsg = 'W';
//按下时传达的数据是'W',即把方向值传给esp32
_pubMessage();
},
backgroundColor: Colors.orangeAccent,
),
),
],
),
new Row(
children: <Widget>[
new Container(
padding: EdgeInsets.fromLTRB(110, 0, 30, 10),
child: FloatingActionButton(
child: new Icon(Icons.arrow_left),
onPressed:(){
_pubMsg = 'A';
_pubMessage();
},
backgroundColor: Colors.orangeAccent,
),
),
new Container(
padding: EdgeInsets.fromLTRB(45, 0, 0, 10),
child: FloatingActionButton(
onPressed: (){
_pubMsg = 'D';
_pubMessage();
},
child: new Icon(Icons.arrow_right),
backgroundColor: Colors.orangeAccent,
),
),
],
),
new Row(
children: <Widget>[
new Container(
padding: EdgeInsets.fromLTRB(175, 0, 0, 0),
child: FloatingActionButton(
onPressed:(){
_pubMsg = 'S';
_pubMessage();
},
child: new Icon(Icons.arrow_drop_down),
backgroundColor: Colors.orangeAccent,
),
)
],
)
],
),
);
}
到此为止,代码部分基本完毕,连接好 esp32 与 ws2812 灯带,给 esp32 供电和联网,再启动 app 就可以开始游戏了。游戏的更多内容可以自行设计。完整代码见项目地址。

浙公网安备 33010602011771号