强化学习AI构建实战 - 基于“黄金点”游戏(二)

服务端接口

为了让大家的AI可以顺利地进行游戏,并验证我们对策略和AI的一些实现,我们需要一些基础设施来帮助我们完成一些工作。这些工作包括游戏回合的控制、参与者之间的数据同步、游戏数据的储存等功能。

为了简化这些基础工作,以便大家可以更好地集中于AI本身的设计和实现,我们搭建了一个服务器提供了一些基本的接口。使用这些接口,AI可以做到简单的调用REST API接口实现游戏回合时间同步、获取历史数据、提交预测数据等功能。

下图描述了服务器如何驱动游戏一回合接着一回合的运转,同时指出了AI或客户端应何时与服务器交互。

当AI或客户端进入游戏后,应立即向服务器请求获取当前回合的状态,此时可以知道服务器上正在进行的游戏回合的编号,以及本回合还有多长时间结束。AI或客户端可以按照返回的回合编号向服务器提交预测值,并且可以根据本回合剩余时间,设定一个定时器,在下一回合开始时,再次执行获取回合状态的接口,来取得下一回合的状态。这样依次轮转下去,AI或客户端就可以一直参与在游戏中。 同时,AI或客户端还可以在每回合开始时,调用获取历史数据的接口,来得到前几回合的比赛数据。这样可以知道自己在上一回合是否得分胜出,并可以根据历史数据来指导当前回合的预测值。

接口概述

服务器地址是https://goldennumber.aiedu.msra.cn,提供RESTful API接口。所有请求需要的参数都拼装在URL中,并且需要对值进行URL编码。所有的响应报文内容都是JSON格式。如果服务器响应代码不是2**或3**,表示该次请求失败。失败的响应报文至少包含一个message属性:

属性名数据类型备注
messageString出错的具体信息

服务端REST接口提供了Swagger描述文档: swagger.json 中文版 英文版

可以参考该API文档直接来调用服务器接口,也可以借助第三方工具从swagger文档生成所需语言的SDK来使用。比如,可以借助SwaggerEditor来生成各种语言版本的客户端SDK,可以极大的方便开发。

另外,服务端也提供了API试用页面,可以方便直接的在线试验API接口。

下面是各个接口的详细描述:

新建玩家

请求方式:GET

路径:/api/NewUser

客户端使用该接口可以新建一个玩家。

请求需要用到的参数:

参数名数据类型是否必需备注
nickNameString可选用户昵称
如果长度超过20,将被截断
建议设置昵称,昵称相对于标识有更好的辨识度

响应报文内容中的属性:

属性名数据类型备注
userIdString用户标识,格式为Guid格式
nickNameString用户昵称

设置用户昵称

请求方式:POST

路径:/api/NickName

使用该接口可以用来修改用户的昵称,昵称相对于标识来说,有更佳的辩识度。

请求需要用到的参数:

参数名数据类型是否必需备注
uidString必需用户标识
nicknameString必需用户昵称,长度大于20会被截断

获取新游戏房间

请求方式:GET

路径:/api/NewRoom

使用该接口创建一个新的游戏房间并获取对应的编号。

请求需要用到的参数:

参数名数据类型是否必需备注
uidstring必需房间创建者的标识
numbersInt可选设置游戏支持的每个玩家可以提交的预测值的个数,目前支持提交1个或2个数
默认是1,表示每个玩家可以提交一个数
durationInt可选设置游戏中每回合的间隔时间
默认值是60秒,取值范围在10~200之间
userCountint可选设置游戏房间中允许的最大玩家数
默认值是0,表示没有限制
有玩家数量限制的房间,当所有玩家都提交预测值后,会立即计算本回合结果,并开始下一轮
注意:这里的玩家数量限制是针对房间的,不是针对一个回合,只要玩家在房间内任一回合提交过预测值,则认为该玩家始终在房间内
roundCountint可选设置比赛总回合数
默认值是0,表示没有限制
如果某一回合没有玩家提交数据,认为该回合无效,不计在回合数内
如果有效回合数达到设置的总回合数,游戏结束,不再允许提交数据
manuallyStartInt可选是否手动开始游戏
默认值0,表示创建完房间后,游戏自动开始
如果是1,表示需要由创建者手动开始游戏

响应报文内容中的属性:

属性名数据类型备注
roomIdInt游戏房间编号

开始游戏

请求方式:GET

路径:/api/StartGame

如果创建游戏时设置的是手动开始,那么游戏创建者可以调用该接口开始游戏。

请求需要用到的参数:

参数名数据类型是否必需备注
uidstring必需房间创建者的标识
roomidint可选房间编号
如果未设置,默认为0号游戏房间

获取游戏状态

请求方式:GET

路径:/api/State

客户端使用该接口可以获取当前房间内的游戏状态,可以根据当前游戏支持提交的预测值的个数进行提交。同时还可以知道当前回合什么时间结束,推算出什么时候可以取得本回合的比赛数据以及获取下一轮比赛的相关信息。

请求需要用到的参数:

参数名数据类型是否必需备注
uidString可选用户标识
roomidInt可选房间编号
如果此参数为空,默认0号房间

响应报文内容中的属性:

属性名数据类型备注
userIdString用户标识
nickNameString用户昵称
roomIdInt房间编号
numbersInt当前房间内的游戏支持提交的预测值的个数,1或2
roundIdstring当前房间内正在进行的游戏回合标识
leftTimeint当前游戏回合还有多少秒截止提交
roundEndTimedatetime当前回合截止提交的UTC时间
stateint当前游戏状态
0代表进行中
1代表未开始,需要房间创建者手动开始
2代表已结束,不允许再向房间内提交数据
hasSubmittedbool当前用户本回合是否已提交预测值
isRoomCreatorbool当前用户是否是当前房间的创建者。
如果房间在创建时没有指定自动开始,需要创建者手动开始游戏
maxUserCountint创建房间时设定的玩家数
0表示没有限制
最大不能超过200
设置人数上限的房间中,在获取格式化的历史数据时,会将未加入游戏的玩家的预测值用0来填补,保证每回合取到的数据都是固定列数的规整数据
同时,设置人数上限的房间中,如果所有玩家都已提交,则立该结束当前回合,并开始下一回合
currentUserCountint当前房间内提交过预测值的玩家数量
totalRoundCountint创建房间时设定的该房间可以进行的有效回合数
finishedRoundCountint当前房间内已经进行的有效回合数
玩家提交过预测值的回合认为是有效回合,否则忽略该回合,继续等待玩家提交
enabledTokenbool当前房间是否已启用身份验证

提交预测值

请求方式:POST

路径:/api/Submit

客户端使用该接口可以向服务器提交预测值。每回合只允许提交一次,提交成功后不可修改。

如果当前房间设置了玩家人数上限,则当所有玩家提交了预测值后,立即计算本回合结果,并开始下一回合。

请求需要用到的参数:

参数名数据类型是否必需备注
ridString必需要提交预测值的回合标识,需要是GUID的格式
uidstring必需提交预测值的用户标识
n1Double必需预测值,必须是0到100之间的有理数,不包括0和100
n2Double可选第二个预测值,如果当前游戏是支持两个数的游戏,此参数也为必需项;如果当前游戏仅支持一个数,此参数将被忽略
tokenstring可选启用身份验证的房间必须带有正确的验证信息才可以提交
由房间创建者提供原始令牌,将用户标识、回合标识、原始令牌连接为新字符串,先做一次SHA256,然后做一次Base64,得到的结果做为token的值

获取黄金点历史数据

请求方式:GET

路径:/api/TodayGoldenList

使用该接口可以获取当前房间内当天的黄金点历史数据,玩家可以基于此来预测下一轮的黄金点值。

请求需要用到的参数:

参数名数据类型是否必需备注
roomidString可选房间编号
如果该参数为空,默认0号房间
roundCountInt可选查询的回合数量
如果此参数为空,默认最近100回合的数据
如果需要查询所有数据,需设置为-1
注意:当回合数特别大时,返回的数据包会特别大

响应报文内容中的属性:

属性名数据类型备注
goldenNumberListArray房间中当天的黄金点历史数据,数组的最后一个值是最新一轮的黄金点值

获取玩家提交的历史数据

请求方式:GET

路径:/api/TodayNumbers

使用该接口可以获取当前房间内,当天已完成的回合中,所有玩家提交的历史数据。

玩家历史数据以数组形式返回,数组中每个元素都有用户索引号和回合索引号,可以按不同维度分别统计某个玩家的提交规律或某个回合详细数据,可以按照自己的需要,对该数据进行建模或训练对应的模型。

请求需要用到的参数:

参数名数据类型是否必需备注
roomidint可选房间编号
如果未设置,默认为0号游戏房间
roundCountInt可选查询的回合数量
如果此参数为空,默认最近100回合的数据
如果需要查询所有数据,需设置为-1
注意:当回合数特别大时,返回的数据包会特别大

响应报文内容中的属性:

属性名数据类型备注
validNumbersint当前房间支持的可提交数字个数,1或者2。
当为1时,下面的数据只有number1是有效的;
当为2时,下面的数据中number1和number2均为有效数字。
numberListarray用户提交的数字列表,数组中的每个元素包含以下属性:
userIndex, roundIndex, number1, number2
    userIndexint用户索引号,相同的用户索引号表示同一个用户在不同回合提交的数字
    roundIndexint回合索引号,相同的回合索引号表示不同用户在同一回合提交的数字
    number1double用户提交的第一个数字
    number2double用户提交的第二个数字,仅当validNumbers为2时有效

获取玩家得分

请求方式:GET

路径:/api/TodayScore

使用该接口可以查询游戏房间内所有玩家的得分情况。用户得分按从高到低排列。

请求需要用到的参数:

参数名数据类型是否必需备注
roomidint可选房间编号
如果未设置,默认为0号游戏房间

响应报文内容中的属性:

属性名数据类型备注
scoreListarray数组中的每个元素包含以下属性:
userId, nickName, score, index
    userIdstring用户标识
    nickNamestring用户昵称
    scoreint得分
    indexint该用户在当前房间内的索引号

获取分页历史数据

请求方式:GET

路径:/api/History

使用该接口可以获取当前房间内的历史数据,包括每回合的黄金点、每个玩家的预测值、得分等信息。

没有指定任何参数时,返回0号房间内最新的10回合的历史。

请求需要用到的参数:

参数名数据类型是否必需备注
roomidString可选房间编号
如果该参数为空,默认0号房间
startridString可选开始查询的游戏回合标识
如果该参数为空,默认为当前正在进行的回合
countInt可选指定从startrid开始返回多少回合的历史,不包括startrid回合
如果没有指定该参数,默认为10,最大不超过100
directionInt可选查询的方向
默认值是0,表示从startrid查询旧的历史数据
另一个值是1,表示从startrid查询更新数据

响应报文内容中的属性:

属性名数据类型备注
roundsArray查询到的回合的数组,数组的每个元素包含以下属性:
roundId, time, goldenNumber, userNumbers
    roundIdString回合标识
    indexint该回合在当前房间中的索引编号
    timeString该回合的截止时间,UTC
    goldenNumberDouble该回合的黄金点
    userNumbersArray该回合所有玩家提交的数的数组,数组的每个元素所含以下属性:
userId, masterNumber, slaveNumber, score
        userIdString用户标识
        masterNumberdouble用户提交的第一个预测值
        slaveNumberdouble用户提交的第二个预测值,仅当当前游戏支持提交两个数的时候有效
        scoreInt用户在当前回合的得分
nickNamesobject用户编号和用户昵称的字典
用户编号是key,用户昵称是value

主体流程分析和实现

从上述的服务器接口描述和定义,我们可以看出,一个最基本的黄金点游戏程序应该具有哪些功能。

要顺利地进行游戏,最核心的两个功能,就是通过服务器提供的RESTful API进行获取当前回合状态GET /api/State)和提交数字POST /api/Submit)。这两个核心操作中,由于提交数字时必须知道当前的游戏设置(如需要提交一个数还是两个数),以及当前的回合ID,所以在提交之前,正确地获取当前回合的状态是必要的。否则提交会失败。有了这两个功能,我们前面列举的一些最简单的游戏策略就可以被实现了,比如提交随机数,提交固定的数等。

当然,上面提到的最简单的两个策略可能不会表现得很好。为此,我们还需要调用获取黄金点历史数据接口(GET /api/TodayGoldenList),这一接口为我们实现诸如重复上一轮的黄金点、计算以前数轮黄金点的均值提供了可能;另外,还可以调用获取玩家提交的历史数据接口(GET /api/TodayNumbers)来得到每个玩家的数据,从而可以推测他人策略、学习历史数据进行建模等。

对于上述三个主要的接口,上文已经对作为其输入的HTTP请求,和服务器输出的数据结构的格式做了一番说明,并提供了Swagger描述,可以方便的生成任何语言的客户端SDK。这里我们对主体流程和相关的要点做一下整理:

  1. 创建新用户或使用已有的userId,获取当前回合状态,初始化客户端环境。包括玩家ID、回合倒计时等;
  2. 尝试在回合倒计时结束前,向服务器提交数据。
    • 提交时必须声明提交针对的回合,已经过去的、不合法的回合会导致提交失败。
  3. 倒计时结束后,获取新一回合——也即当前回合——的状态。
    • 由于时间同步可能不稳定,只有观察到回合ID较之前改变了,新回合才算开始。
  4. 回到第2步。

伪代码如下:

// 任一房间号。省略的话,默认是0号房间。
roomId=42// 初始化。
user = NewUser(nickName="foo")
userId = user.userId // 整个循环中,只使用一个用户ID,以正确统计得分。// 游戏主循环。
while true:   // 获取当前游戏的状态,包括当前回合的标识号,回合结束的剩余时间等state = GetState(uid=userId, roomId=roomId) // 获取当前房间的历史记录。默认是从当前回合开始追溯。history = GetTodayGoldenList(roomId) // 尝试在当前回合结束前,向服务器提交数据。while DateTime.now < state.roundEndTime:if HaveNotSubmittedForRound(state.roundId):// 核心:策略实现。num = Calculate(history) Submit(num, uid=userId, rid=roundId) // 提交数据必须指定提交者ID和目标回合ID。while true:nextState = GetState(uid=userId, roomId=roomId) // 使用当前用户ID,避免分配新ID。// 直到确认新回合开始了,才更新客户端状态。if nextState.roundId != state.roundId: state = nextStatebreak

我们也提供了C#和Python的几个Bot示例代码,大家可以尝试运行一下。

游戏策略

前面介绍了服务器的接口及BOT程序的主要框架,下面将讨论下可以使用哪些策略来玩这个游戏。

基于规则的策略

当我们思索游戏的制胜策略时,我们想到或许一些简单的策略就能取得出乎意料的效果。比如人在思索游戏对策的时候,经常想到的一种对策就是基于规则的,其表现为一系列“当对方……我就……”形式的条件和计划。当游戏的对手确实执行了我们预测中的操作时,我们从预先定义的计划里找到对应的手段并执行。这种基于规则的策略往往要求我们先尽可能地预测对手的行为,并做出针对性的压制计划。

典型的基于规则的策略有:重复上一回合的黄金点、上一回合黄金点乘以0.618、取最近十轮中黄金点的均值、使用固定值如42、生成(0,100)区间内的随机数等等。或者稍微复杂一些的规则,比如前面我们提到2018年微软学生夏令营中同学们的策略:当黄金点的取值连续多轮变化不大时,用一个比较大的值拉高黄金点的值,另一个值可以适应比前一轮黄金点高一些。

Azure Notebooks中的BotDemo.ipynb就是一个简单的基于规则的Bot,可以在线运行试一下。

基于识别的策略

基于规则的策略能达到的成就或许有限,因为我们处于一个复杂且多变的游戏环境,我们可能很难有效地制定一些应对性的规则。除了简单规则之外,我们还可以尝试的一种手段是基于识别的策略,即我们可以先尝试识别对手使用的是怎样的策略,然后再预测其在下一轮比赛中可能采取的行动,然后再做针对性的应对。比如,如果我们识别了对手采取的是“重复上一轮黄金点”这一基于规则的策略,我们就能有效地进行应对(如提交任意比上轮黄金点小的数)。我们可以看出,这种策略比起基于规则的策略有着更为超前的视野。然而,考虑到游戏本身的复杂性,我们能有效识别的敌方策略,可能会很有限。

基于强化学习的策略

除了上述的策略,我们还可以采用基于强化学习的策略。观察过几组游戏的历史记录后,我们注意到这些历史记录中似乎是存在一定规律的,但是其潜在的规律难以用简单的规则或手工组织的程序来捕捉。对此,我们可以尝试通过强化学习的方式来完成这一任务。

以前面介绍的Q-Learning为例,首先我们要确定可能的状态和动作,但很明显,这里的状态空间和动作空间都是无限大的,我们尝试强行作个简化然后再使用Q-Learning。

对于状态空间来说,我们可以考虑将黄金点数据用特定的编码方式编码,如将黄金点的趋势转换为“上升/下降X%”的形式,其中X可以是几个固定的值如10、20、50等。更简单的还可以将最近几回合的黄金点趋势总结为上升/下降了几次。

对于动作空间来说,一种简单的处理是将前面基于规则和策略拿过来,多定义几种规则作为多个动作。

最后以这个有限的状态和动作来构造Q表进行强化学习的训练。

Azure Notebooks中的RLBotDemo.ipynb就是一个简单的使用Q-Learning的Bot,可以在线运行试一下。

必须要提到的一点是,上面这种简化并不是特别合理,有时训练出来的模型可能比不过单纯基于简单规则的,所以需要大家尝试定义不同的状态空间和动作空间,看是否能玩的更好。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/625743.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VM虚拟化——物理机迁移至虚拟化

一、安装迁移工具 VMware vCenter Converter Standalone 【安装向导】 【最终用户专利协议】 【最终用户许可协议】 【安装位置】 【安装类型】默认本地安装 【用户体验设置】 【准备安装】 二、迁移 【转换机器】 【源主机】 填ip、用户名和密码 最好是用administ…

什么是DDOS高防ip?DDOS高防ip是怎么防护攻击的

随着互联网的快速发展&#xff0c;网络安全问题日益突出&#xff0c;DDoS攻击和CC攻击等网络威胁对企业和网站的正常运营造成了巨大的威胁。为了解决这些问题&#xff0c;高防IP作为一种网络安全服务应运而生。高防IP通过实时监测和分析流量&#xff0c;识别和拦截恶意流量&…

PattPatel-“Introduction to Computing Systems“(4)期末样卷题目解析:C语言递归

C语言的递归我觉得最主要的还是要把Patt&Patel的部分好好理解下&#xff08;因为有和硬件结合的部分&#xff09;&#xff0c;但因为今天就考试&#xff08;来不及做这样的事情&#xff09;&#xff0c;先把之前模拟卷的题目给尝试弄明白&#xff0c;然后考完试之后继续学习…

vue-ESlint代码规范及修复

1. 介绍 ESLint:是一个代码检查工具&#xff0c;用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。 在创建项目时&#xff0c;我们使用的是 JavaScript Standard Style 代码风格的规则。 规范网址&#xff1a;https://standardjs.com/rules-zhcn.htm…

力扣-刷MySQL(详细解析)

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

uniApp 顶部导航栏右侧添加文字按钮

{"path" : "pages/allin/MessageCenter/MessageCenter","style" : {"navigationBarTitleText": "消息中心","enablePullDownRef…

如何在Eclipse IDE中安装TestNG插件

目录 使用Eclipse Marketplace安装TestNG插件 通过输入URL安装TestNG 1.点击安装新软件 2.输入URL以安装TestNG 3.遵循正常的安装过程 4.重新启动Eclipse 在Eclipse中安装TestNG插件的视频 在这篇文章中&#xff0c;我们将介绍如何在Eclipse IDE中安装TestNG插件&#x…

JVM-Arthas高效的监控工具

一、arthas介绍 3.选择监控哪个进程 4.进入具体进程 二、arthas的基础命令与基本操作 1.查询包含Java的系统属性&#xff1a; 命令&#xff1a;sysprop |grep java 1.查询不含Java的系统属性&#xff1a; 命令&#xff1a;sysprop | grep -v java 3.打印历史命令 命令&#…

C语言从入门到实战——结构体与位段

结构体与位段 前言一、结构体类型的声明1.1 结构体1.1.1 结构的声明1.1.2 结构体变量的创建和初始化 1.2 结构的特殊声明1.3 结构的自引用 二、 结构体内存对齐2.1 对齐规则2.2 为什么存在内存对齐2.3 修改默认对齐数 三、结构体传参四、 结构体实现位段4.1 什么是位段4.2 位段…

基于springboot的流浪动物救助管理系统

&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;一 、设计说明 1.1研究背景 随着…

windows平台高dpi介绍

flutter在windows平台如何自定义dpi设置 系统层级的支持(windows平台对高dpi的支持) 主要有两点&#xff1a; 设置系统的缩放比例 (系统及系统自带的app会根据这个设置来进行缩放&#xff1b;自己的app需要结合自己设置的dpi awareness来实现对应的dpi支持)设置进程的dpi aw…

BIOS知识枝桠——RAID 磁盘阵列

文章目录 前言一、RAID介绍二、RAID等级分类1.RAID02.RAID13.RAID24.RAID3和RAID45.RAID5和RAID66.RAID77.RAID10 BIOS下组建RAID 前言 假设存在多块磁盘&#xff0c;如果不组建阵列&#xff0c;磁盘与磁盘之间是没有任何关系的。磁盘A和B&#xff0c;放在A中的文件与B磁盘没有…

vue中使用component中的is渲染组件如何使用,:is 等价 v-if渲染组件。

动态组件顾名思义动态加载不同的组件&#xff0c;is属性用于加载不同组件&#xff0c;传参使用属性传递 1、使用v-for遍历component&#xff0c;组件都会执行 <componentv-for"(item, index) in TAB_PANE":key"index":is"item.componentName"…

Java多线程——并发和并行、实现方法

多线程 并发和并行 实现方法 代码演示 方式一 package com.qiong.thread1;public class MyThread extends Thread{Overridepublic void run() {for (int i 0; i < 20; i) {System.out.println(getName() "Hello World");}} }package com.qiong.thread1;public…

运筹说 第65期 | 动态规划的基本概念和基本原理

20世纪50年代初&#xff0c;美国数学家R. Bellman 等人在解决多阶段决策优化问题时提出了一种高效的求解方法——动态规划&#xff08;Dynamic Programming&#xff09;&#xff0c;该方法基于多阶段决策优化问题的特点&#xff0c;把多阶段问题转换为一系列互相联系的单阶段问…

2024抖店选品方法,及侧重方向思路(全新版本),可收藏备用

我是王路飞。 做无货源抖店的商家&#xff0c;牢记【选品重于泰山】这句话。 要知道电商的本质就是产品&#xff0c;你所有的运营手段也都是围绕产品进行的&#xff0c;店铺内的流量也都是冲着产品来的。 产品不行&#xff0c;哪怕再多的流量、再高的曝光率&#xff0c;也带…

2024年全网最全春招时间线

2024年全网最全春招时间线 春招&#xff0c;许多同学可能会误以为这是春天才会进行。 你可能会想&#xff0c;期末刚考完试&#xff0c;先享受下寒假&#xff0c;再欢度春节&#xff0c;收些红包&#xff0c;甚至还能抽空去理个发型。等到春日明媚时&#xff0c;再参加春招活…

linux docker安装 rustdesk

这里写自定义目录标题 1&#xff1a;软件介绍&#xff1a;2&#xff1a;安装1. 服务器端2. 客户端 3&#xff1a;配置5&#xff1a;其他1:rustdesk 官方Docker Compose 1&#xff1a;软件介绍&#xff1a; 名称作用官网项目地址rustdesk实现多端互控https://rustdesk.com/inde…

图书管理系统:从数据库设计到前端展示的实战经验分享

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Arduino| 串口通讯、入门示例

Arduino串口通讯 为什么要做串口通讯串口通讯原理串口通讯函数字符串常用函数串口通讯示例入门示例测试串口通讯复杂指令处理 为什么要做串口通讯 串口通讯&#xff1a;串口通信是用来在不同电子设备之间交换数据用的技术&#xff0c;其实就是要实现不同电子设备之间的“通讯对…