跟我一起使用 compose 做一个跨平台的黑白棋游戏(3)状态与游戏控制逻辑

news2024/11/28 16:34:22

前言

在上一篇文章中,我们已经完成了黑白棋的界面设计与编写,今天这篇文章我们将完成状态控制和游戏逻辑代码的编写。

正如第一篇文章所述,在本项目中,我们需要实现不依赖于平台的状态管理,也就是使用 Flow 和 composable 来实现。

另外,还是再声明一下,这个项目的 AI 算法来自于 reversi 项目。

老规矩,先上游戏效果:

s1.gif

(ps:哈哈,这个GIF原本有8mb,但是我给压缩成了300kb,而且还是在没有改变分辨率和帧率的情况下,画质的损失也还能接受。想知道怎么压缩的可以看我之前的文章有说哦)

开始实现

解答上次留下的问题

上一篇文章,我们留下了一个问题,为什么要自己指定使用屏幕宽度而不是直接使用 fillMaxWidth

其实这个问题只要自己写一遍就知道为什么。

因为如果使用 fillMaxWidth 的话,在 Canvas 中返回的 size.height 会是 0。

或许你会说,那我直接使用 fillMaxSize 不就不会是 0 了吗?

你品品这话,难道你的界面中只有一个棋盘?其他组件不要了?哈哈。

状态提升与界面修改

在正式开始编写逻辑代码前,我们需要先将上次实现的界面中的状态抽出来,做一个状态提升:

@Composable
fun GameView(
    chessBoard: Array<ByteArray>,
    playerChessNum: Int,
    aiChessNum: Int,
    gameState: Int,
    aiLevel: AiLevel,
    whoFirst: Int,
    onClickChess: (row: Int, col: Int) -> Unit,
    onRequestNewGame: () -> Unit,
    onNewGame: (whoFirst: Int, aiLevel: AiLevel) -> Unit,
    onTip: () -> Unit
) { 
    // ……
}

这个函数中的参数全是之前实现的界面中需要用到的状态数据,这里我们把它们都提出来作为这个函数的参数。

对应的,我们需要把之前界面中写死的状态改为使用这些参数,以实现动态更新。

另外,我们需要加两个 Dialog 用于提示游戏结束和新建游戏。

在 GameView() 中添加:

// 游戏结束弹窗
if (gameState >= 3) {
    RequestNewGameDialog(gameState) {
        onRequestNewGame()
    }
}

// 新游戏弹窗
if (gameState == NeedNewGame) {
    NewGameDialog(onStart = { whoFirst: Int, aiLevel: AiLevel ->
        onNewGame(whoFirst, aiLevel)
    })
}

新创建两个 composable:

@Composable
private fun RequestNewGameDialog(gameState: Int, onStart: () -> Unit) {
    val text = when (gameState) {
        3 -> "恭喜,你赢了!"
        4 -> "抱歉,电脑赢了"
        5 -> "游戏结束,这次是平局哦"
        else -> "游戏结束"
    }
    Dialog(onDismissRequest = { }) {
        Card(backgroundColor = Color.White) {
            Column(modifier = Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
                Text(text = text, fontSize = 24.sp, modifier = Modifier.padding(vertical = 6.dp))
                Button(onClick = { onStart() }) {
                    Text(text = "重新开始")
                }
            }
        }
    }
}

@Composable
private fun NewGameDialog(onStart: (whoFirst: Int, aiLevel: AiLevel) -> Unit) {
    var isPLayerFirst by remember { mutableStateOf(true) }
    var aiLevel by remember { mutableStateOf(AiLevel.Level1) }

    Dialog(onDismissRequest = { }) {
        Card(backgroundColor = Color.White) {
            Column(
                modifier = Modifier.padding(8.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Checkbox(
                        checked = isPLayerFirst,
                        onCheckedChange = { isPLayerFirst = !isPLayerFirst })
                    Text(text = "玩家先手")
                }

                Text(text = "AI难度")

                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier.padding(vertical = 6.dp)
                ) {
                    RadioButton(
                        selected = aiLevel == AiLevel.Level1,
                        onClick = { aiLevel = AiLevel.Level1 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level1.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level2,
                        onClick = { aiLevel = AiLevel.Level2 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level2.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level3,
                        onClick = { aiLevel = AiLevel.Level3 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level3.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level4,
                        onClick = { aiLevel = AiLevel.Level4 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level4.showName)
                }

                Row(verticalAlignment = Alignment.CenterVertically) {
                    RadioButton(
                        selected = aiLevel == AiLevel.Level5,
                        onClick = { aiLevel = AiLevel.Level5 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level5.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level6,
                        onClick = { aiLevel = AiLevel.Level6 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level6.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level7,
                        onClick = { aiLevel = AiLevel.Level7 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level7.showName)

                    RadioButton(
                        selected = aiLevel == AiLevel.Level8,
                        onClick = { aiLevel = AiLevel.Level8 },
                        modifier = Modifier.size(26.dp)
                    )
                    Text(text = AiLevel.Level8.showName)
                }


                Button(
                    onClick = {
                        onStart(if (isPLayerFirst) PLayerRound else AiRound, aiLevel)
                    },
                    modifier = Modifier.padding(6.dp)
                ) {
                    Text(text = "开始")
                }
            }
        }
    }
}

上面两个 Dialog 显示效果如下,

游戏结束:

s2.png

创建新游戏:

s3.png

逻辑结构

首先看一下完成后的代码结构:

s4.png

为了避免混淆,我把界面相关的包打码了。

可以看到,核心逻辑就两个包:gameLogic 和 viewModel。

其中,gameLogic 包下的 AlgorithmRule 两个类是复制自大佬的AI算法代码。这里有一个坑需要注意一下,因为大佬的代码是 java 写,为了统一代码,我使用 AndroidStudio 的自动转换功能自动转成了 kt ,然而自动转换有点问题,可能会转出错,所以最好是自己手动写一遍,或者转了之后再检查一下,改一改。

另外两个类是之前编写的几个辅助工具类。

而 viewModel 下的内容则是状态控制的核心代码。

下面将一一进行讲解。

GameState

这个类是对当前游戏中所有用到的状态的封装:

data class GameState (
    /**当前棋盘上的棋子信息**/
    val chessBoardState: ChessBoardState = ChessBoardState(),
    /**AI难度等级**/
    val aiLevel: AiLevel = AiLevel.Level1,
    /**游戏状态**/
    val gameState: Int = PLayerRound,
    /**先手**/
    val whoFirst: Int = PLayerRound,
    /**AI当前棋子数量**/
    val aiChessNum: Int = 2,
    /**玩家当前棋子数量**/
    val playerChessNum: Int = 2,
)

enum class AiLevel(val showName: String, val level: Int) {
    Level1("菜鸟", 1),
    Level2("新手", 2),
    Level3("入门", 3),
    Level4("棋手", 4),
    Level5("棋士", 5),
    Level6("大师", 6),
    Level7("宗师", 7),
    Level8("棋圣", 8),
}

data class ChessBoardState (
    val chessBoardArray: Array<ByteArray> = initChessBoard(),
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ChessBoardState

        if (!chessBoardArray.contentDeepEquals(other.chessBoardArray)) return false

        return true
    }

    override fun hashCode(): Int {
        return chessBoardArray.contentDeepHashCode()
    }
}

其中每个状态表示什么用途我在注释中已经说明。

需要注意的一点是,对于状态 chessBoardState 我没有直接声明类型为 Array<ByteArray> 而是使用另外一个 data class 封装了一下,为什么要这样呢?

这是因为 kotlin 中虽然会自动为 data class 生成 equalshashCode 等方法,但是如果参数中包含可变类型,例如 Array 、 list 等,则自动生成会失效,需要我们自己手动写一下。

data class ChessBoardState 中也可以看到,我确实重载了这两个方法。

回到我们的问题,如果我们不单独抽出一个 data class 来存放这个 Array 的话,一旦我们修改了这个 data class 的参数(删除或新增),就必须同步的修改 equalshashCode 这个两个方法,不然会出现意想不到的错误。

然而不是每次修改参数我们都会记得同时去修改这两个方法,我之前就因为这个问题被坑了。

将可变参数抽出来单独封装的话,可以保证这个抽出来的类只有这一个参数,我们也不会去动它,这就避免了修改参数时忘记修改方法。

GameAction

这个类是用于承载 view 向 gamePresenter 发送事件的类:

sealed class GameAction {
    /**请求开启新的游戏**/
    object ClickRequestNewGame : GameAction()
    /** 请求提示**/
    object ClickTip : GameAction()
    /**点击了棋盘的某一个格子**/
    data class ClickChess(val row: Int, val col: Int) : GameAction()
    /**开始一个新的游戏**/
    data class CLickNewGame(val whoFirst: Int, val aiLevel: AiLevel): GameAction()
}

每个事件的作用我已经使用注释标明。

有一点需要注意,新建游戏使用到了两个事件: ClickRequestNewGameClickNewGame

其中,ClickRequestNewGame 只是用于更改状态以在 view 中显示新建游戏的 Dialog。

接收到 ClickNewGame 事件后才会真正的初始化状态并按照用户选择的参数开启一局新的游戏。

gamePresenter

这个类是用于处理从 view 接收到的事件:

@Composable
fun gamePresenter(
    gameAction: Flow<GameAction>,
): GameState {
    var gameState by remember { mutableStateOf(GameState()) }

    LaunchedEffect(gameAction) {
        gameAction.collect { action: GameAction ->
            when (action) {
                is GameAction.ClickChess -> {
                    val newState = clickChess(gameState, action.row, action.col)

                    if (newState != null) {
                        gameState = newState

                        withContext(Dispatchers.IO) {
                            // 电脑下子
                            gameState = runAi(gameState)
                            // 检查游戏是否已结束
                            gameState = checkIfGameOver(gameState)
                        }
                    }
                }
                is GameAction.ClickRequestNewGame -> {
                    gameState = gameState.copy(
                        gameState = NeedNewGame
                    )
                }
                is GameAction.ClickTip -> {
                    // TODO 暂时不写这个
                }
                is GameAction.CLickNewGame -> {
                    gameState = GameState(
                        whoFirst = action.whoFirst,
                        aiLevel = action.aiLevel,
                        gameState = action.whoFirst
                    )

                    if (action.whoFirst == AiRound) {
                        withContext(Dispatchers.IO) {
                            // 电脑下子
                            gameState = runAi(gameState)
                        }
                    }
                }
            }
        }
    }

    return gameState
}

在这个类中,我们使用 Flow 接收发送过来的新流,并在接收到新流后做出相应的操作,更新 gameState 的值,由于 gameState 被托管给了 mutableStateOf

所以当 gameState 被改变时会触发 compose 的重组机制,导致重新调用这个函数,发送新的状态给使用到的地方,从而让所有使用到了这个状态的地方全部重组,这样,UI就会对应的更新。

在 MainActivity 中我们是这样调用的:

setContent {
    ReversiChessComposeTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colors.background
        ) {
            val channel = remember { Channel<GameAction>() }
            val flow = remember(channel) { channel.consumeAsFlow() }
            val state = gamePresenter(flow)

            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                GameView(
                    chessBoard = state.chessBoardState.chessBoardArray,
                    playerChessNum = state.playerChessNum,
                    aiChessNum = state.aiChessNum,
                    gameState = state.gameState,
                    aiLevel = state.aiLevel,
                    whoFirst = state.whoFirst,
                    onClickChess = { row: Int, col: Int ->
                        channel.trySend(GameAction.ClickChess(row, col))
                    },
                    onRequestNewGame = {
                        channel.trySend(GameAction.ClickRequestNewGame)
                    },
                    onNewGame = { whoFirst: Int, aiLevel: AiLevel ->
                        channel.trySend(GameAction.CLickNewGame(whoFirst, aiLevel))
                    },
                    onTip = {
                        channel.trySend(GameAction.ClickTip)
                    }
                )
            }
        }
    }
}

gamePresenter 中实现开始新游戏很简单,所以我们就不过多赘述,大家看代码即可。

我们着重说说玩家下子后的处理:

val newState = clickChess(gameState, action.row, action.col)

if (newState != null) {
    gameState = newState

    withContext(Dispatchers.IO) {
        // 电脑下子
        gameState = runAi(gameState)
        // 检查游戏是否已结束
        gameState = checkIfGameOver(gameState)
    }
}

首先我们调用 clickChess 检查下子是否合法,并更新 UI,如果不合法则返回 Null:

private fun clickChess(gameState: GameState, row: Int, col: Int): GameState? {
    // 黑白棋规则是黑子先手,所以如果是AI先手的话,意味着玩家执白子
    val playerColor: Byte = if (gameState.whoFirst == PLayerRound) BlackChess else WhiteChess

    // 判断是否是玩家回合
    if (gameState.gameState != PLayerRound) {
        return null
    }

    // 下子区域不合法
    if (!Rule.isLegalMove(gameState.chessBoardState.chessBoardArray, Move(col, row), playerColor)) {
        return null
    }

    // FIXME 这里有一个BUG,可能会出现玩家已无棋可走,但是没有继续跳回AI或者结束游戏导致"卡死"
    val legalMoves = Rule.getLegalMoves(gameState.chessBoardState.chessBoardArray, playerColor)
    if (legalMoves.isEmpty()) { // 玩家已经无棋可走
        return gameState.copy(
            gameState = AiRound
        )
    }

    val move = Move(col, row)
    // 调用该方法后会更新传入的 chessBoardArray 并返回关联更改的棋子信息
    val moves = Rule.move(gameState.chessBoardState.chessBoardArray, move, playerColor) // TODO moves 可以用来做动画效果

    // 计算棋子数量
    val statistic: Statistic = Rule.analyse(gameState.chessBoardState.chessBoardArray, playerColor)

    return gameState.copy(
        chessBoardState = ChessBoardState(gameState.chessBoardState.chessBoardArray),
        gameState = AiRound,
        playerChessNum = statistic.PLAYER,
        aiChessNum = statistic.AI
    )
}

并且在玩家无路可走时则直接跳到 AI 下子。

完成玩家下子后则是 AI 下子:

private suspend fun runAi(gameState: GameState): GameState {
    val delayTime: Long = Random(System.currentTimeMillis()).nextLong(200, 1000)
    delay(delayTime) // 假装AI在思考(

    val aiColor: Byte = if (gameState.whoFirst == AiRound) BlackChess else WhiteChess

    val legalMoves: Int = Rule.getLegalMoves(gameState.chessBoardState.chessBoardArray, aiColor).size

    if (legalMoves > 0) {
        val move: Move? = Algorithm.getGoodMove(
            gameState.chessBoardState.chessBoardArray,
            Algorithm.depth[gameState.aiLevel.level],
            aiColor,
            gameState.aiLevel.level
        )
        if (move != null) {
            val moves = Rule.move(gameState.chessBoardState.chessBoardArray, move, aiColor) // TODO moves 可以用来做动画效果

            // 计算棋子数量
            val statistic: Statistic = Rule.analyse(gameState.chessBoardState.chessBoardArray, (-aiColor).toByte())

            return gameState.copy(
                chessBoardState = ChessBoardState(gameState.chessBoardState.chessBoardArray),
                gameState = PLayerRound,
                playerChessNum = statistic.PLAYER,
                aiChessNum = statistic.AI
            )
        }
    }

    return gameState.copy(
        gameState = PLayerRound
    )
}

AI 开始计算前会先有一个随机的延时,模拟AI思考的过程(

哈哈,其实这里的延时是为了用户体验,因为计算非常快,几乎几毫秒就算好了,如果不加延时,在用户眼中看到的则是我下了之后怎么没反应?其实不是没反应,只是在用户下完了之后AI瞬间就下好了,导致用户会产生AI一直不动的错觉。

其实这里如果加上动画的话体验会更好,但是动画的内容咱们还是放到后面的亿点点优化中去做吧。

AI 下子完成后就开始判断当前游戏状态是否已结束:

private fun checkIfGameOver(gameState: GameState): GameState {
    val aiColor: Byte = if (gameState.whoFirst == AiRound) BlackChess else WhiteChess
    val playerColor: Byte = if (gameState.whoFirst == PLayerRound) BlackChess else WhiteChess

    val aiLegalMoves: Int = Rule.getLegalMoves(gameState.chessBoardState.chessBoardArray, aiColor).size
    val playerLegalMoves: Int = Rule.getLegalMoves(gameState.chessBoardState.chessBoardArray, playerColor).size

    if (aiLegalMoves == 0 && playerLegalMoves == 0) {
        // 两方都无子可走,游戏结束
        val statistic = Rule.analyse(gameState.chessBoardState.chessBoardArray, playerColor)
        val newState = if (statistic.AI > statistic.PLAYER) GameOverWithAi
                        else if (statistic.AI < statistic.PLAYER) GameOverWithPLayer
                        else GameOverWithTie

        return gameState.copy(
            gameState = newState
        )
    }

    return gameState
}

这里判断游戏结束的方法很简单,就是检查当前玩家或者AI能否继续下子,如果都不能下子则统计各自的棋子数量判定游戏胜负。

总结

自此,所有游戏控制逻辑编写完成!现在这个游戏已经完全是可以玩的程度了,我也试了下,有点难度,不好下啊。

但是目前还存在以下几个问题:

  1. 界面布局不美观
  2. 游戏提示功能还没做(这个其实很好做,获取到所有可以合法下子的格子,然后更新一个提示UI到棋盘上就可以了)
  3. 逻辑上还有点问题,例如,当玩家已经无法继续下子时应该跳到AI下子或开始判定游戏结果,但是现在如果出现这种情况会直接"卡死"

当然,这些问题都无伤大雅,所以我决定先暂时不去改这些,下一步我们先将其移植到桌面端,然后再慢慢的做亿点优化。

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

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

相关文章

浏览器扩展一些好用插件

给浏览器添加一些插件功能&#xff0c;能够让我们用的更方便&#xff0c;开发中非常实用&#xff0c;下面直接开始 我们这里选择的是微软自带的Microsoft Edge浏览器&#xff08;谷歌也行。这两款浏览器都是非常好用的&#xff09; 我们打开浏览器找到扩展应用这个&#xff0c;…

opencv 中值滤波

中值滤波是一种常用的图像滤波算法&#xff0c;是在像素点周围进行多个点的中值滤波&#xff0c;将点的灰度值根据其周围像素点的灰度值进行平均&#xff0c;并使这些点的灰度值具有相似性&#xff0c;以达到平滑去噪的目的。中值滤波在图像处理中应用广泛&#xff0c;在图像滤…

利用java编写的项目设备调配系统代码示例(内含5种设备调配的算法)

利用java编写的项目设备调配系统代码示例&#xff08;内含5种设备调配的算法&#xff09; 一、设备调配方案二、设备匹配算法三、代码实现&#xff08;java&#xff09; 最近在做一个项目设备调配系统&#xff0c;分享一些干货&#xff01;&#xff01;&#xff01; 一、设备…

Godot引擎 4.0 文档 - 入门介绍 - Godot 关键概念概述¶

本文为Google Translate英译中结果&#xff0c;DrGraph在此基础上加了一些校正。英文原版页面&#xff1a;Overview of Godots key concepts — Godot Engine (stable) documentation in English Godot 关键概念概述 每个游戏引擎都围绕您用来构建应用程序的抽象展开。在 Godo…

【mysql】库的操作+表的操作

文章目录 启动mysql登录mysql1.MySQL环境安装与基础认识修改端口号连接服务器服务器&#xff0c;数据库&#xff0c;表关系建表 第二讲_库与表的操作1.创建数据库2.创建数据库案例3.指明字符集和校验集校验规则对数据库的影响不区分大小写的查询以及结果&#xff1a;区分大小写…

SQL Backup Master 6.3.6 Crack

SQL Backup Master 能够为用户将 SQL Server 数据库备份到一些简单的云存储服务中&#xff0c;例如 Dropbox、OneDrive、Amazon S3、Microsoft Azure、box&#xff0c;最后是 Google Drive。它能够将数据库备份到用户和开发者的FTP服务器上&#xff0c;甚至本地机器甚至网络服务…

速通二次型、二次型标准型、二次型规范型

浅过二次型 理解二次型可以从二次型的多项式入手&#xff1a; 显然&#xff0c;在系数都为实数的情况下&#xff0c;二次型矩阵即为一个实对称矩阵。 取一个代入值的例子就是&#xff1a; 二次型的标准型 OK&#xff0c;再从二次型的标准型的多项式入手&#xff0c;如下&…

FPGA System Planner(FSP)使用手册

FSP工具是cadence公司为了FPGA/PCB协同设计而推出的一个解决方案工具包。它的主要工作是由软件来自动生成、优化FPGA芯片的管脚分配,提高FPGA/PCB设计的工作效率和连通性。FSP完成两顷重要工作:一、可以自动生成FPGA芯片的原理图符号(symbol);二、自动生成、优化和更改FPG…

C++模板(上)

文章目录 模板函数模板函数模板的实例化 类模板总结 模板 模板是C种为了方便用户对于一些场景的使用&#xff0c;引入的新概念&#xff0c;使得我们的代码不会冗余 template关键字 template关键字的意思就是模板&#xff0c;语法为&#xff1a;template<typename T1,type…

内网渗透之Linux权限维持-OpenSSHPAM后门SSH软链接公私钥登录

0x01替换版本-OpenSSH后门 原理&#xff1a;替换本身操作系统的ssh协议支撑软件openssh&#xff0c;重新安装自定义的openssh,达到记录帐号密码&#xff0c;也可以采用万能密码连接的功能&#xff01; 可以修改软件版本和删除安装记录 1.环境准备&#xff1a; yum -y install…

【Java EE 初阶】网络初识

目录 1.网络互连 1.局域网&#xff1a; 2.广域网WAN 2.网络通信基础 3.IP地址&#xff1a;端口号 4.协议 1.五元组 2.协议分层 1.为什么要用网络分层&#xff1f; 3.OSI七层模型 4.TCP/IP五层&#xff08;或四层&#xff09;模型 5.封装和分用 1.应用层 2.传输层A…

Oracle数据库中了locked1勒索病毒攻击后怎么办,什么是locked1勒索病毒

Oracle数据库是一种被集团企业广泛使用的关系型数据库管理系统&#xff0c;但是随着科学技术的不断发展&#xff0c;在现代互联网环境中数据库安全性成为了一个非常重要的问题。而其中主要的威胁就是勒索病毒攻击。一旦数据库被勒索病毒攻击入侵&#xff0c;许多重要的数据就会…

【JVM】3. 运行时数据区及程序计数器

文章目录 3.1. &#x1f379;运行时数据区3.1.1. &#x1f942;概述3.1.2. &#x1f942;线程3.1.3. &#x1f942;JVM系统线程 3.2. &#x1f379;程序计数器(PC寄存器) 3.1. &#x1f379;运行时数据区 3.1.1. &#x1f942;概述 本节主要讲的是运行时数据区&#xff0c;也就…

跟我一起使用 compose 做一个跨平台的黑白棋游戏(2)界面布局

前言 在上一篇文章中&#xff0c;我们讲解了实现这个游戏的总体思路&#xff0c;这篇文章我们将讲解如何实现游戏界面。 本文将涉及到 compose 的自定义绘制与触摸处理&#xff0c;这些内容都可以在我往期的文章中找到对应的教程&#xff0c;如果对这部分内容不太熟悉的话&am…

论文阅读_语音合成_VALL-E

论文阅读 number headings: auto, first-level 2, max 4, _.1.1 name_en: Neural Codec Language Models are Zero-Shot Text to Speech Synthesizers name_ch: 神经网络编解码器语言模型实现零样本TTS paper_addr: http://arxiv.org/abs/2301.02111 date_read: 2023-04-25 da…

Docker代码环境打包进阶 - DockerHub分享镜像

1. Docker Hub介绍 Docker Hub是一个广泛使用的容器镜像注册中心&#xff0c;为开发人员提供了方便的平台来存储、共享和分发Docker容器镜像。它支持版本控制、访问控制和自动化构建&#xff0c;并提供了丰富的公共镜像库&#xff0c;方便开发人员快速获取和使用各种开源应用和…

Redis+Lua脚本防止超卖

超卖就是因为查询库存和扣减库存两个操作不是原子性操作&#xff0c;通过rua脚本执行这两个操作可以保证这两个操作原子性 判断库存量是不是大于等于1&#xff0c;如果大于等于1对库存减1&#xff0c;否则就不去减库存 StringBuilder sb new StringBuilder();sb.append("…

【数据分享】我国地级市绿地利用现状数据(9个指标\Shp格式)

绿地是城市生态的重要组成部分&#xff0c;在很多分析中都会用到绿地数据&#xff01;之前我们分享过Shp和Excel格式的全国地级市2003-2020年绿地面积数据&#xff08;可查看之前文章获悉详情&#xff09;&#xff0c;以及中国31个主要城市的绿地空间分布的栅格数据&#xff08…

vue中使用colorthief获取图片的主色调成分

colorthief官网 https://lokeshdhakar.com/projects/color-thief/#examples 安装 npm i --save colorthief yarn add colorthief 使用案例 <template><div class"box app" :style"{ background: bodyBgColor }"><div class"img-item&…

NSS LitCTF Web 部分wp

目录 1、PHP是世界上最好的语言&#xff01;&#xff01; 2、这是什么&#xff1f;SQL &#xff01;注一下 &#xff01; 3、Ping 4、作业管理系统 5、我Flag呢&#xff1f; 6、1zjs 7、Vim yyds 8、Http pro max plus 1、PHP是世界上最好的语言&#xff01;&#xff01…