在之前的文章里边别离介绍了运用Compose DeskTop开发一个秒表运用和挂钟运用,虽说是运用可是功能略显单一,今日咱们把难度略微调高一点,来完成一个桌面版的五子棋小游戏,其间除了涉及到之前讲过的Compose Canvas制作的知识点以外,还参加了运用MVI形式完成页面交互与视图更新,以及五子棋算法剖析,终究简略完成一个人机对战功能
制作棋盘
棋盘长什么姿态大家应该心里都清楚,便是一个n * n的网格布局,在Canvas
里边就相当于在x轴跟y轴方向各均匀的制作n-1条线,为什么是n-1呢,由于边边上画不画线都相同,所以咱们第一步先把制作棋盘所需求的变量界说出来
- gridCount : 网格数量,可自界说
- screenW : 画布的宽度,在Canvas中运用size.width更新值
- screenY : 画布的高度,在Canvas中运用size.height更新值
- xUnit : 每一格的宽度
- yUnit : 每一格的高度
- xList : 保存一切点的x坐标
- yList : 保存一切点的y坐标
在画布里边,棋盘的巨细一般根本都是铺满整个窗口的,所以这儿运用fillMaxSize
,画布色彩咱们挑选一个略微偏黄一点的色彩究竟棋盘大都情况下都是木质的
在画布里边咱们先给screenW
和screenY
两个变量赋值,然后开端画棋盘上的网格,网格的制作相当于便是遍历xList
和yList
,每一次遍历就在对应的x坐标与y坐标方位别离画上横线与竖线,线的长度或许高度便是screenW
或许screenY
网格的制作部分就完毕了,咱们在Main.kt的里边参加这个Fivekids
的Composable函数,代码如下
简略的设置了一下标题和窗口的方位,运转一下后咱们的棋盘就出来了
棋子
制作棋子的进程跟棋盘就不相同了,棋子是动态的,是依据鼠标点击棋盘某一个方位的时分,判别点击方位是否在有用方位才开端 在该方位地点的坐标区域画棋子,这儿涉及到几个问题需求考虑下
- 棋子是个圆,圆的半径巨细怎样确定?太大了两个棋子之间或许会重叠,太小了在棋盘上展现的作用就不会很好
- 怎样确定点击的方位归于哪个区域?要知道从咱们方才制作好的棋盘上看,横线竖线相交的当地都是制作棋子的圆心,而点击的方位能够是恣意当地,咱们需求将点击方位与某一个圆心相关起来
- 用什么方法将制作好的棋子保存起来?由于终究是需求去核算制作好的棋子是否是五个相连而且同色,所以至少需求一个数组将这些棋子所对应的坐标保存起来
第一个问题,咱们能够调查下棋盘,假如需求让两棋子刚刚好摆放在两个有用方位之间,由于棋子圆心就在白线相交方位,所以咱们能够理解为俩棋子的半径加一同刚好等于xUnit
或许yUnit
,为什么是或许?由于咱们的棋盘地点的窗口纷歧定是正方形的,所以导致里边的小方格也纷歧定是正方形,所以一个棋子的直径应该刚好是xUnit
和yUnit
的较小值,如下所示
变量chessRadius
便是棋子的半径,那么第一个问题就处理了,咱们看第二个问题,怎样确定点击方位归于哪个区域,由于咱们现已确定好了棋子的半径,那么是不是能够这样考虑,点击方位归于某一个区域的条件是这个区域的中心点xy坐标减去点击方位xy坐标的绝对值小于半径的话,那么阐明棋子就应该在这个区域制作,所以咱们能够事先将一切区域的棋子先制作出来,将色彩设置为通明,当某一个区域经过遍历满意被制作的条件今后,该区域的棋子再设置为黑色或许白色,因此需求创立一个棋子的实体类,该类代表着一个棋子的x坐标,y坐标以及当时展现的色彩
然后创立一个数组,这个数组里边永久保存着棋盘上一切的SingleChess
,并在初始化的时分将这个数组里边的SingleChess
的black
值设置为0,即通明
随后咱们就在Canvas
里边将这些点经过drawCircle
逐一制作出来,而且依据black
值的不同,将棋子显示不同的色彩
现在这个状况咱们一切的棋子都仍是通明的,所以在棋盘上是看不到棋子的,想要看到的话那肯定得下棋啊,下棋咱们就要经过点击棋盘获取点击方位了,而咱们的棋盘便是Canvas
,Compose里边经过运用Modifier
操作符的pointerInput
函数来获取点击的坐标,代码如下
在PointerInputScope
中的detectTapGestures
函数里边就能够拿到点击坐标了,咱们先创立两个变量用来保存每一次点击获取的坐标,并在detectTapGestures
里边对它们赋值
点击坐标有了,那么咱们假如想要在点击完成今后显示对应的棋子,那么就要在方才对pointList
赋值的当地经过判别点击方位的xy坐标是否与pointList
里边某一个SingleChess
的xy坐标相差在chessRadius
范围之内,是的话就add进去一个黑棋或许白棋,不是的话仍是add进去一个通明的棋子,所以咱们还需求一个值来表明当时应该是下黑棋仍是白棋,而且将最新下完的棋也保存在一个变量里边,代码如下
black
是个布尔值,默认为true,表明黑棋先下,然后在下完黑棋,也便是在pointList
里边增加完black
值为1的SingleChess
之后,将black
设置为false,表明该白棋下了,point
是最新下完的棋,下面是完好的下棋代码
现在咱们点击棋盘今后,棋子就会出来了,运转一下看看作用
做点交互
棋子的制作做完了,但究竟是游戏,咱除了根本的下棋以外,也得有些交互,而咱们这个五子棋小游戏的交互暂时我想出了以下几点
- 窗口中得有个当地提示当时应该轮到白棋下仍是黑棋下
- 白棋取胜或许黑棋取胜的时分,窗口中增加一段“恭喜某方取胜”的提示语
- 纷歧定非得比及某一方棋子到了五个才决出输赢,有时分棋盘上呈现比方“双活三”,”双冲四”的这样的必败局势,得有个当地让玩家自动认输,而且呈现“某方认输,某方取胜”这样的提示语
- 一局竞赛完毕之后,需求有个当地让玩家自动建议再来一局,然后棋盘上棋子清空,先手回到黑棋这边
那么要完成这些交互的话,咱们窗口里边除了棋盘,就要多几样元素了,别离是提示哪一方开端下棋的状况位,显示提示语的案牍,以及认输和再来一局这俩按钮,接下去咱们就在Main.kt里边加上这些元素
咱们在棋盘的上方增加了一个Row
布局,其间Surface
用来显示当时是哪一方开端下棋,而且咱们把它设置为圆形,相当于便是一个棋子相同,放在Row
的最左面,Text
用来展现提示语,在Row
的正中间,最右边是认输和再来一局两个按钮,咱们看下实际作用
黑白棋状况
咱们现在的状况仍是写死的色值,假如需求完成黑棋下完,状况变成白棋,白棋下完状况变成黑棋的话,需求让状况监听棋盘里边哪一方刚下完,那么这儿能够运用MVI的形式去完成这个交互,棋盘便是咱们唯一的数据发送源,再创立个UI State类叫ChessState
ChessState
类里边维护着一个用来改写状况视图的TurnSide
类,它接纳一个布尔值的参数,很明显当棋盘里边下完黑棋之后,TurnSide
发送false,当下完白棋之后,TurnSide
发送true,那么发送的事情咱们就交给StateFlow
,先在Main.kt里边创立个StateFlow
然后再创立个记载当时状况色彩的变量
spaceColor
便是用来展现状况的色值,它需求依据chessState
监听TurnSide
传递过来的值来决定是显示黑色仍是白色,StateFlow
监听数据是一个挂起函数,所以要把它放在一个协程环境里边,咱们这儿再增加上LaunchedEffect
函数,把监听数据的操作放在LaunchedEffect
中进行,代码完成如下
这段代码就能够完成依据棋盘传递过来的黑白棋状况来展现不同色值,那么咱们在哪里发送这个事情呢?没错,就在咱们pointList
增加完黑白棋状况的SingleChess
的方位,所以咱们这边需求把chessState
这个StateFlow
当作参数传递给咱们棋盘的函数Fivekids
然后在下完黑白棋的方位用StateFlow
把事情发送出去,像这样
现在咱们的黑白棋状况现已能够动态的依据下完棋今后动态改动了,咱们看下实际作用
显示提示语
提示语是在某一方认输,某一方取胜以及其他情况下显示在棋盘上方正中间,所以也是依据棋盘里边发送出来的状况来显示的,那么咱们在方才创立的UI State类里边还需求加上一个提示语的状况类
而咱们之前在Text组件里边是写死的一个空字符串,现在要让它显示的案牍发生变化,所以得先创立个案牍的变量用来保存当时是显示的什么案牍
变量title
还要去监听来自棋盘里边发出来的案牍,所以在LaunchedEffect
里边再加上对案牍的监听
这儿给这段代码做个测验,比方方才咱们发送黑白棋状况的方位,这儿将发送黑白棋状况的代码改为发送案牍,相同也表明哪一方下完轮到另一方开端下,代码如下
咱们再看下作用
提示语的展现也处理完了,现在咱们来把两个按钮的点击事情也处理下
“认输”和“再来一盘”
认输按钮的功能是点击今后,棋盘依据哪一方建议的认输事情,来发送对应提示语,假如是黑方建议的认输,那么发送案牍“黑方认输,白方取胜”,反之则发送“白方认输,黑方取胜”,可是发送案牍这个事情自身是依据外部按钮点击触发的,所以咱们这儿需求从外部传递一个状况到棋盘里边来告诉应该发送案牍事情了,外部传递进来的状况咱们就界说为
gameState
的默认值为0,表明为正常下棋状况,然后咱们在点击认输按钮的时分,将gameState
设置为1,表明为某一方认输了,一同在函数Fivekids
函数里边增加一个Int
类型的gameState
参数,每一次gameState
值改动的时分,触发Fivekids
重组
当点击认输按钮今后,棋盘上是不能再持续竞赛了,也便是gameState
为1的时分,发送对应案牍的一同,将棋盘设置为不能再有新的棋子呈现的,代码如下
咱们看到除了发送对应的案牍,咱们在下棋的当地也增加了新的判别,只要当gameState
为0的时分,才能够有新的棋子呈现,其他状况点击棋盘上恣意方位,黑白棋的数量坚持不变,再看下作用
认输按钮的功能完成了,然后接着处理再来一盘的功能,点击这个按钮的时分,棋盘上的棋子需求清空,先手方回到黑方,然后将案牍也置空,所以咱们也需求在UI State类里边新增一个状况来改写点击完再来一盘按钮后的窗口视图,新的状况类界说如下
然后在再来一盘的点击事情中,将gameState
设置为2,表明此刻竞赛需求重新开端
在棋盘内,当接纳到gameState
为2的时分,将pointList
清空,黑白棋状况设置为黑方,并发送重置状况
此刻在LaunchedEffect
中新增对ResetState
的处理,将黑白棋状况变成黑色,提示语置空,gameState
变成0,重新回到下棋状况
再看下作用
现在一切交户都开发完成了,现在就要考虑下怎样去判别哪一方取胜了
怎样判别哪一方取胜
下过五子棋的都知道,判别一方取胜的条件是只要当相邻的五个点呈现同色的棋子才算赢,能够是横着排,也能够是竖着排,斜着也能够,规则非常容易,可是怎样在咱们的棋盘上去完成这个规则呢?咱们需求考虑的问题其实只要一个,怎样判别棋盘上呈现了接连五个同色的棋子,同色的好完成,只需求用一个filter
操作符将pointList
中black
为1或许2的SingleChess
过滤出来就能够了,咱们就能得到只要黑棋或许只要白棋的子list,代码如下
现在咱们就把这个问题简化到了怎样在一个list中判别有五个相邻的坐标点,好家伙~是简化了但不多,硬生生的给自己组织了一道算法题,没办法只能烧点脑细胞了
区间算法
首要想到的是先将数组里边的点依照x坐标排一下序,运用sortedBy
操作符,然后下标从0开端,五个五个判别它们的x,y相减是否等于咱们的xUnit
或许yUnit
值,相当于便是判别一个区间里边的坐标了
如上图所示,先判别绿框的点,然后再判别红框的点,但很快就验证了这个做法的问题,由于就算依照x坐标排序,但遇到同一个x坐标下有超过一个点的情况就有问题了,会把真实相邻的点圈在了框子的外面
Map算法
经过上面的考虑,我又想着把同一个x坐标下的一切点放在一个新的数组里边,同一个y坐标下的一切点也放在一个新的数组下,然后别离保存在一个Map
里边,那么只需求遍历这个Map
,再将每个list依照y坐标或许x坐标排序,这样假如水平方向或许笔直方向呈现五个接连的坐标点的话,就能够找出来了,但也只能满意这两个方向,假如需求判别斜着的方向,就得一同遍历五个相邻的数组,这算法的功率就很低了,假如棋子的数量慢慢增加的话,会发现点击棋盘时分,有很明显的卡顿
分散算法
这个是休息了一个晚上想出来的,所以说算法这东西一个思路或许比代码更重要,咱们之前的思路是把棋盘上一切的点都考虑进去,然后从里边找五个接连的棋子,其实仔细想想没有很大的必要,由于下棋的时分,咱们都会去找像活三,冲四这样的棋子组合,再往里边下咱们最新的棋子来组成五个,所以咱们每次判别的起始点是咱们棋盘上最新下的点,然后往四周分散出去查找需求的点是否在咱们的list里边,咱们看下面这张图
中间这个点永久都是棋盘上最新更新的点,然后对它的x,y坐标别离依照水平,笔直,左上至右下,左下至右上四个方向进行增加或许减去单位长度,每一次核算完今后,把新的坐标点与list中一切坐标点逐一比较,假如坐标共同而且色彩共同,阐明存在一个相邻的点,累加器加一,直到累加器的值变成五今后,阐明咱们棋盘上有五个相邻的点,点代表的色彩一方取胜,现在咱们就把这个思路转换成代码,新增一个函数judgeList
,回来值是个布尔值,用来回来一个List
里边是否存在五个相邻的棋子
其间pList
是只存在黑色棋子的数组或许只存在白色棋子的数组,point
是最新参加pointList
的棋子,xUnit
和yUnit
别离是x轴方向和y轴方向的单位长度,当pList
长度小于5的时分,咱们不核算,大于5的时分,别离从countHorizontalFive
,countVerticalFive
,countLTtoRBFive
,countRTtoLBFive
四个函数判别各个方向上是否有五个接连相邻的棋子,先看核算水平方向的函数
对于水平方向来讲,point
的y坐标不参加核算,x坐标首要向右走一格,然后得到的点经过samePoint
函数判别是否满意要求,满意的话计数器加一,然后再往右走一格,直到4次遍历完毕,假如计数器没有到5函数没有被return,那么再往左持续相同的核算,假如计数器到5了,阐明水平方向存在接连五个点,假如没有到5,那么持续履行其他方向的函数,至于repeat4次的原因,是由于对于某一个点来讲,假如周围存在接连五个点,那么单一方向上最多只会存在四个点,所以遍历4次就够了,咱们再看下samePoint
函数是怎样判别得到的点存在于pList
看到这儿的判别很简略,除了判别色彩共同,便是在遍历list的时分,针对每一个list里边的点的xy坐标与核算出来的xy坐标相减,得到的结果取绝对值假如小于等于3,那么证明是一个点,至于为什么是小于等于3而不是直接等于0,由于咱们一切的坐标在参加核算的时分都会从Float
转成Int
,这个进程或许存在精度上的误差,所以把差值定为3是考虑到了精读问题,其他方向上的逻辑都根本相同,代码就不贴了,根本便是把水平方向的造几个轮子下去,再改几个变量就好,咱们终究一步便是调用咱们的judgeList
函数,代码如下
这样咱们在棋盘上核算哪一方取胜的逻辑就写完了,看下作用怎样吧
人机对战
现在是完成了自己与自己下棋,但作为一个竞技类游戏,五子棋仍是要真实对战起来才有意思,可是假如参加电脑的话,怎样让电脑判别应该在哪里下棋是个难点,咱们知道一个凶猛的电脑,下棋时分是能够判别什么应该进攻什么时分应该防卫,也能够识别棋盘上是否有冲四或许活三这样的棋局,我这边只能暂时先完成个弱一点的电脑,先能够在棋子周围的有用区域下棋
什么时分应该轮到电脑下
之前是在Canvas
里边经过点击事情记载坐标来完成黑白棋互相下棋的,那么现在咱们必须下完棋子今后,要比及电脑操作完完今后才能持续下,所以咱们需求用black
标识符来做个开关,当这边黑棋下完今后,开关关闭,这个时分无论怎样点击棋盘,得到的坐标都是无效的,比及电脑下完今后再把开关翻开
咱们看到当black
为false的时分,此时点击棋盘不会有任何反应的,由于xy坐标都为0,这个时分应该轮到电脑下了,电脑需求判别棋盘上一切空的方位里边哪里最适宜下,这个是比较耗时的操作,所以咱们需求把这些操作放在协程里边,这儿就需求运用LaunchedEffect
函数,在函数里边经过生成一个Flow
,在Flow
的上游核算适宜的下棋点,在下流把点传给tapX
与tapY
,代码如下
LaunchedEffect
函数里边履行了经过两秒今后,传递一个随机点给下流,咱们看到参数传了个black
,那是由于LaunchedEffect
函数只要当参数发生改动,才会再一次履行它里边的代码块,所以选择运用black
作为入参,由于black
会在每次下完棋今后改动一下值,现在再看下作用
现在当咱们下完黑棋的时分能够发现,咱们鼠标没有移动,白棋现已由电脑下好了,然后咱们只需求将生成随机点的代码改成真实去核算适宜方位的代码就好了
核算电脑下棋方位
电脑应该下在什么当地的核算方法同终究断定哪一方输赢的思路是共同的,都是需求经过判别某一个点的周围是否存在黑子或许白子,所以棋盘上那些没有被下过棋的点能够分为两大类,一类是周围没有棋子,一类是周围有棋子,而终究电脑需求下棋的方位便是从那些周围有棋子的空白方位处选一个,那么首要咱们把棋盘上一切的点分红三个调集
入参是咱们的pointList
,然后在咱们新建的函数里边,将pointList
分红空白方位点,黑棋方位点以及白棋方位点的调集,接下去要做的便是遍历整个emptyList
,将里边每个点周围存在几个白棋或许几个黑棋核算出来,为此,咱们SingleChess
需求新增两个属性
每一个空白点都会在核算后标志出周围有几个黑棋或许几个白棋
终究优先从emptyList
中过滤出周围有棋子的空白方位,随机出一个交给棋盘去渲染,没有的话就从一切空白方位随机一个
而每一个空白点周围有几个黑棋或许白棋,就在函数whetherInList
里边进行
需求核算出四个数字,别离是某一个方位水平,笔直,左上至右下,左下至右上方向上存在多少个同色棋子,然后比较得出最大值,恣意一个方向上核算方法同终究判别输赢的方法类似,不同的是,这边是先一向判别一侧,直到没有带色彩的棋子的时分,再去判别另一侧,然后终究回来计数器的值,比方核算左上至右下方向的代码
当两个开关都变为false的时分,计数器核算出来的值便是某一个点在该方向上周围存在的某一个色彩的棋子个数,其他方向上的核算方法类似,咱们就跳过了,直接将pickRightPoint
放入LaunchedEffect
函数中
这边的参数还多加了xUnit
和yUnit
,由于假如不增加这两个参数,终究pickRightPoint
函数中的xUnit
和yUnit
都只会为初始值0,现在在跑下代码,看看跟电脑下棋的作用
总结
这个demo写的时刻算是比较长的,一共花了一个礼拜时刻,主要是算法这边卡了会,然后核算电脑下棋的方位也卡了会,细心的小伙伴或许有发现,其实这套代码终究设计是计划将电脑下棋的方位分个优先级,whiteCount
>3的优先级最高,究竟表明马上就要取胜了,blackCount
>3的优先级其次,表明防卫,然后优先级慢慢轮下去,但这样做总是会让白棋与黑棋到了必定数量今后就重叠制作了,所以只能暂时先弄一个超简易版的电脑,后边处理了再更新一篇,然后还会测验一下局域网对战,等开发出来了一同分享给大家