004-Kotlin界面开发快速入水之TicTacToe

news2024/11/5 0:14:52

在这里插入图片描述

程序界面和效果

在这里插入图片描述

快速入水

要学习一样跟程序设计有关的东西,最好的办法始终是把手打湿,整一个能够运行,可以实验的东西出来。

也只有在程序开发中,我们才能想一个魔法师而不是魔术师,我们真的能够创造一个东西。而且编译器不会因为我们很丑、我们学历不行、我们没女朋友、我们很穷而拒绝我们,只要我们严格按照手册,就真的可以梦想成真。真是幸运啊!

所以,我们先来写一个简单的程序,一个大家都完全不会感兴趣的游戏:井字棋。

这个游戏实在是无聊,我从来没有成功说服任何一个人跟我一起好好玩过……不过……无聊也是它的好处,马上就行。

构建工具链

现代化的程序设计语言通常都有完整的工具链,负责完成:

  • 管理工程-文件的组织
  • 编译源代码
  • 运行、运行、分发程序

热门但是无法流行的伟大Rust语言,它的工具链是cargo

不热门但是很流行的C/C++语言,它的工具链是make,现在也有cmake,或者ninja

热门流行的Java语言,它的工具链是antmavengradle

Kotlin是基于Java的,它的工具链我们一般选择gradle。实际上gradlemavenant都是Java的构建工具。

gradle构建Kotlin程序,最好玩的是,我们可以编写kotlin来完成。当然,以前还用groovy,现在大概可能或许提倡用kotlin

构成一个gradle工程的文件有:

  • settings.gradle.kts:工程的设置
  • build.gradle.kts:工程的构建
  • src:源代码目录
  • gradle.properties:gradle的属性文件
  • gradlewgradlew.bat:gradle的wrapper的启动脚本
  • gradle/wrapper目录:gradle的配置文件和wrapper
    • gradle-wrapper.jar
    • gradle-wrapper.properties

这是在IDEA中一个典型工程的结构:

在这里插入图片描述

这里展示了全部的文件,包括隐藏文件:

  • .gradle:gradle的缓存目录
  • .idea:IDEA的配置目录
  • build:gradle的构建目录
  • .kotlin:kotlin的配置目录
  • .run:运行配置目录

有些时候,还有:

  • out:编译输出目录

当然,这些都不需要自己来创建,只需要在IDEA中创建一个新的gradle工程就行了。甚至,下面大部分配置都不需要自己来创建,只需要安装一个Jetpack Compose的插件,然后新建一个Compose工程就行了。

但是,我们这里还是絮絮叨叨一下,可以大概知道这些都是什么。

Gradle Wrapper

为了让工程更加有移植性,我们一般会使用gradle wrapper,这个工具会自动下载指定版本的gradle,并且把gradle的启动脚本放在工程的根目录下。

我们可以通过gradlew或者gradlew.bat来启动gradle,这样就不需要在系统中安装gradle了。

这相关的几个文件,我们可以通过gradle wrapper命令来生成。

gradle wrapper

这个命令会生成gradlewgradlew.batgradle/wrapper/gradle-wrapper.jargradle/wrapper/gradle-wrapper.properties这几个文件。

在最后那个文件中,通常包含有下载的地址和版本号。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

我们通(bi)常(xu)更改那个distributionUrl地址,指向我们自己的镜像源,这样下载会快一些。

gradle.properties

这个文件通常用来存放一些工程的属性,比如版本号、插件版本等等。

org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
kotlin.code.style=official
kotlin.version=2.0.0
compose.version=1.6.10

第一行的org.gradle.jvmargsgradle的启动参数,这里设置了堆内存和文件编码。

在调用gradle的时候,我们可以通过-P参数来传递这些属性,比如:

gradle build -Pkotlin.version=1.5.31

这里设定的属性,可以在build.gradle.kts文件中使用。

settings.gradle.kts

我们需要在settings.gradle.kts文件中添加一些内容,这个文件是一个Kotlin脚本,用来描述工程的设置。

pluginManagement {
    repositories {
        maven("https://maven.aliyun.com/repository/public")
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
        google()
        gradlePluginPortal()
        mavenCentral()
    }

    plugins {
        kotlin("jvm").version(extra["kotlin.version"] as String)
        id("org.jetbrains.compose").version(extra["compose.version"] as String)
        id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String)
    }
}

rootProject.name = "Demo004"

这个文件首先描述了插件的管理,然后指定了工程的名称。

当然,我们也在这里添加了一些镜像源,这样下载插件会快一些。这个技能必须要掌握……不掌握就会觉得体验极其糟糕……什么都打不开,什么都运行不了。

build.gradle.kts

我们需要在build.gradle.kts文件中添加一些内容,这个文件是一个Kotlin脚本,用来描述工程的构建过程。

import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.targets.js.npm.fromSrcPackageJson

plugins {
    kotlin("jvm")
    id("org.jetbrains.compose")
    id("org.jetbrains.kotlin.plugin.compose")
}

group = "org.cardc.fdii"
version = "1.0.0"

repositories {
    maven("https://maven.aliyun.com/repository/public")
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    google()
}

dependencies {
    // Note, if you develop a library, you should use compose.desktop.common.
    // compose.desktop.currentOs should be used in launcher-sourceSet
    // (in a separate module for demo project and in testMain).
    // With compose.desktop.common you will also lose @Preview functionality
    implementation(compose.desktop.currentOs)

    // Gson dependency
    implementation("com.google.code.gson:gson:2.11.0")
}

compose.desktop {
    application {
        mainClass = "MainKt"

        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "Demo004"
            packageVersion = "1.0.0"
        }
    }
}

这里面,通常只更改了几处:

  • group:工程的组织
  • version:工程的版本,通常我会从1.0-SNAPSHOT改成1.0.0,前者在分发的时候会有问题
  • repositories:镜像源,这里添加了一些镜像源,同样!这个技能必须要掌握!
  • dependencies:依赖,这里compose.desktop.currentOs,这是Compose Desktop的依赖,如果是Compose工程,自动就有;后面的gson是一个JSON库,我们可能会用到。

src目录

这个目录是源代码目录,我们可以在这个目录下创建kotlinjavaresources等等目录,用来存放源代码和资源文件。

当然,以这个工程为例,我们只需要在src/main/kotlin目录下创建一个Main.kt文件,这个文件就是程序的入口。

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.useResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.io.File

@Composable
fun TicTocTile(
    x: Int,
    y: Int,
    ticTacToe: TicTacToe
) {

    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {

        Button(modifier = Modifier
            .padding(5.dp)
            .fillMaxSize(),
            colors = ButtonDefaults.buttonColors(backgroundColor = ticTacToe[x, y].color()),
            onClick = {
                if (ticTacToe.isGameOver()) {
                    ticTacToe.startNewGame()
                    return@Button
                }

                ticTacToe.nextMove(x, y)

            }) {
            Text(
                text = ticTacToe.textAt(x, y),
                fontSize = 36.sp,
                color = Color.Black
            )
        }
    }
}


@Composable
@Preview
fun TicToc() {
    val board = remember {
        mutableStateMapOf<Pair<Int, Int>, Player>(
            Pair(0, 0) to Player.NULL,
            Pair(0, 1) to Player.NULL,
            Pair(0, 2) to Player.NULL,
            Pair(1, 0) to Player.NULL,
            Pair(1, 1) to Player.NULL,
            Pair(1, 2) to Player.NULL,
            Pair(2, 0) to Player.NULL,
            Pair(2, 1) to Player.NULL,
            Pair(2, 2) to Player.NULL
        )
    }
    val player = remember { mutableStateOf(Player.X) }

    val ticTacToe = TicTacToe(board, player)

    Column {
        for (i in 0..2) {
            Row {
                for (j in 0..2) {
                    TicTocTile(i, j, ticTacToe)
                }
            }
        }
        Text(
            modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
            text = ticTacToe.gameText(),
            fontSize = 30.sp,
            color = ticTacToe.color(),
            textAlign = TextAlign.Center
        )

    }

}

fun main() = application {
    useResource("config.json") {
        config = loadConfig(it)
    }

    Window(
        onCloseRequest = ::exitApplication,
        title = "Tic Tac Toe",
        state = rememberWindowState(
            position = WindowPosition.Aligned(Alignment.Center),
            size = DpSize(320.dp, 400.dp)
        )
    ) {
        Box(
            modifier = Modifier.background(Color.White).fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            TicToc()
        }

    }
}

然后是TicTacToe的实现:

enum class Player {
    NULL, X, O;

    fun nameString(): String {
        return when (this) {
            X -> "X"
            O -> "O"
            else -> ""
        }
    }


}

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.SnapshotStateMap


data class TicTacToe(val board: SnapshotStateMap<Pair<Int, Int>, Player>, val player: MutableState<Player>) {
    fun gameText(): String {
        val winner = winner()
        if (isGameOver()) {
            if (winner == Player.NULL) {
                return "Game Over Tie."
            }
            return "Player ${winner.nameString()} won!"
        }
        return "Player ${player.value.nameString()}'s turn"
    }


    fun winner(): Player {
        for (i in 0..2) {
            if (get(i, 0) != Player.NULL && get(i, 0) == get(i, 1) && get(i, 1) == get(i, 2)) {
                return get(i, 0)
            }
            if (get(0, i) != Player.NULL && get(0, i) == get(1, i) && get(1, i) == get(2, i)) {
                return get(0, i)
            }
        }
        if (get(0, 0) != Player.NULL && get(0, 0) == get(1, 1) && get(1, 1) == get(2, 2)) {
            return get(0, 0)
        }
        if (get(0, 2) != Player.NULL && get(0, 2) == get(1, 1) && get(1, 1) == get(2, 0)) {
            return get(0, 2)
        }
        return Player.NULL
    }


    fun nextMove(x: Int, y: Int) {
        if (isTaking(x, y)) return
        board.put(Pair(x, y), player.value)
        nextPlayer()
    }

    operator fun get(x: Int, y: Int): Player {
        return board[Pair(x, y)] ?: Player.NULL
    }

    fun textAt(x: Int, y: Int): String {
        return get(x, y).nameString()
    }

    fun startNewGame() {
        for (key in board.keys) {
            this.board.put(key, Player.NULL)
        }
        player.value = Player.X
    }

    fun isGameOver(): Boolean {
        return winner() != Player.NULL || isFilled()
    }

    private fun isFilled(): Boolean {
        board.values.find { it == Player.NULL }?.let {
            return false
        }
        return true
    }

    private fun isTaking(x: Int, y: Int): Boolean {
        return get(x, y) != Player.NULL
    }

    private fun nextPlayer() {
        player.value = if (player.value == Player.X) Player.O else Player.X
    }


}

为了配合显示颜色,我们还需要一个扩展函数:

import Player.O
import Player.X
import androidx.compose.ui.graphics.Color
import com.google.gson.Gson
import java.io.File
import java.io.InputStream


data class Config(
    val PlayerXColor: String="00FF00",
    val PlayerOColor: String="#0000FF",
    val GameOverColor: String="##FF0000"
)

fun loadConfig(file: InputStream): Config {

    try {
        val json = file.readAllBytes().decodeToString()
        return Gson().fromJson(json, Config::class.java)
    } catch (e: Exception) {
        e.printStackTrace()
        return Config("#FF0000", "#0000FF", "#00FF00")
    }
}

val String.color
    get() = Color(removePrefix("#").toInt(16)).copy(alpha = 1f)

lateinit var config: Config

fun Player.color(): Color {
    return when (this) {
        X -> config.PlayerXColor.color
        O -> config.PlayerOColor.color
        else -> Color.White
    }
}

fun TicTacToe.color(): Color {
    if (isGameOver()) {
        return config.GameOverColor.color
    }
    return player.value.color()
}

这个负责从config.json文件中读取颜色配置,然后在游戏中对不同玩家显示不同颜色,并给出游戏结束时的颜色。

这个config.json文件的内容是:

{
  "PlayerXColor": "#00FF00",
  "PlayerOColor": "#0000FF",
  "GameOverColor": "#FF0000"
}

这样,我们就完成了一个简单的井字棋游戏。

运行

在windows下面,我们可以通过gradlew.bat来运行这个程序:

./gradlew.bat run

还能够通过gradle来构建这个程序:

./gradlew.bat createDistributable

这样就会在build/compose/binaries目录下生成一个可执行文件程序文件夹,里面包含了可执行文件和依赖库。

这个文件拷贝到其他地方,就可以运行了。

当然在IDEA中——默认你使用的是IDEA——你可以直接右边的Gradle工具栏中选择Tasks,然后选择application,然后选择run,就可以运行这个程序。

在这里插入图片描述

看看这个游戏的显示效果,其实还有点小清新的……特别是下棋的时候那个点击的爽感,还是不错的……

总结

这个程序是一个简单的井字棋游戏,我们通过Jetpack Compose来实现了界面,通过Kotlin来实现了逻辑。

至于程序的实现细节,源代码的解读,就放在下次。

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

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

相关文章

Node.js:Express 服务 路由

Node.js&#xff1a;Express 服务 & 路由 创建服务处理请求req对象 静态资源托管托管多个资源挂载路径前缀 路由模块化 Express是Node.js上的一个第三方框架&#xff0c;可以快速开发一个web框架。本质是一个包&#xff0c;可以通过npm直接下载。 创建服务 Express创建一…

C语言 | Leetcode C语言题解之第530题二叉搜索树的最小绝对差

题目&#xff1a; 题解&#xff1a; void dfs(struct TreeNode* root, int* pre, int* ans) {if (root NULL) {return;}dfs(root->left, pre, ans);if (*pre -1) {*pre root->val;} else {*ans fmin(*ans, root->val - (*pre));*pre root->val;}dfs(root->…

重学SpringBoot3-整合 Elasticsearch 8.x (二)使用Repository

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 整合 Elasticsearch 8.x &#xff08;二&#xff09;使用Repository 1. 环境准备1.1 项目依赖1.2 Elasticsearch 配置 2. 使用Repository的基本步骤2.1 创建实体类2.2 创…

SpringBoot源码(四):run() 方法解析(一)

run()方法&#xff1a; public ConfigurableApplicationContext run(String... args) {// 记录应用启动时间long startTime System.nanoTime();DefaultBootstrapContext bootstrapContext createBootstrapContext();// 创建 ConfigurableApplicationContext 对象Configurabl…

ASP .NET CORE 6 在项目中集成WatchDog开源项目

概念 WatchDog是一个开源的项目&#xff0c;可以实现对.Net 应用程序和API实现实时应用日志和性能监控平台。可以实现实时记录和查看应用程序中的消息、事件、HTTP请求和响应&#xff0c;以及运行时捕获的异常&#xff0c;有效帮助开发人员去排查应用异常&#xff0c;提升开发效…

分类算法——决策树 详解

决策树的底层原理 决策树是一种常用的分类和回归算法&#xff0c;其基本原理是通过一系列的简单决策&#xff0c;将数据集划分为多个子集&#xff0c;从而实现分类。决策树的核心思想是通过树形结构表示决策过程&#xff0c;节点代表特征&#xff0c;边代表决策&#xff0c;叶子…

python 使用进程池并发执行 SQL 语句

这段代码使用了 Python 的 multiprocessing 模块来实现真正的并行处理&#xff0c;绕过 Python 的全局解释器锁&#xff08;GIL&#xff09;限制&#xff0c;从而在多核 CPU 上并发执行多个 SQL 语句。 from pyhive import hive import multiprocessing# 建立连接 conn hive.…

[ 问题解决篇 ] win11中本地组策略编辑器gpedit.msc打不开(gpedit.msc缺失)

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

[Python学习日记-55] 软件开发目录设计规范

[Python学习日记-55] 软件开发目录设计规范 简介 为什么要设计好目录结构&#xff1f; 目录组织方式 关于 README 的内容 关于 setup.py 和 requirements.txt 关于配置文件的使用方法 简介 我们在浏览一些开源项目或者是一些安装后的软件的时候会发现&#xff0c;不同的两…

18.农产品销售系统(基于springboot和vue的Java项目)

目录 1.系统的受众说明 2.开发环境与技术 2.1 Java语言 2.2 MYSQL数据库 2.3 IDEA开发工具 2.4 Spring Boot框架 3.系统分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 经济可行性 3.1.3 操作可行性 3.2 系统流程 3.2.1 操作流程 3.2.2 登录流程 3.2.3 删除信…

嵌入式常用功能之通讯协议1--IIC

嵌入式常用功能之通讯协议1--串口 嵌入式常用功能之通讯协议1--IIC&#xff08;本文&#xff09; 嵌入式常用功能之通讯协议1--SPI 一、IIC总线协议介绍 Inter-Integrated Circuit(集成电路总线&#xff09;&#xff0c;是由 Philips 半导体公司&#xff08;现在的 NXP 半导体…

一位纯理科生,跨界自学中医,自行组方治好胃病、颈椎病与高血脂症,并在最权威的中国中医药出版社出版壹本专业中医图书!

这是一位铁杆中医迷&#xff0c; 也是《神农本草经——精注易读本》的作者。 希望更多的人能够受到启发&#xff0c;感受中医之神奇&#xff0c;敢于跨界&#xff0c;爱好中医&#xff0c;学习中医&#xff01; 一个病人以自己的切身感受与诊断&#xff0c;并使之汤药治愈疾病&…

java项目之个人博客系统的设计与实现(springboot)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的闲一品交易平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; springboot个人博客系统的…

使用 Sortable.js 库 实现 Vue3 elementPlus 的 el-table 拖拽排序

文章目录 实现效果Sortable.js介绍下载依赖添加类名导入sortablejs初始化拖拽实例拖拽完成后的处理总结 在开发过程中&#xff0c;我们经常需要处理表格数据&#xff0c;并为用户提供便捷的排序方式。特别是在需要管理长列表、分类数据或动态内容时&#xff0c;拖拽排序功能显得…

使用Kafka构建大规模消息传递系统

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Kafka构建大规模消息传递系统 引言 Kafka 简介 安装 Kafka 创建主题 生产者 消费者 高级特性 分区 持久化 消费者组 消息确认…

队列(Queue)的介绍与实现

文章目录 队列队列的概念及结构 队列的实现初始化队列销毁队列队尾入队列队头出队列获取队列头部元素检测队列是否为空获取队列中有效元素个数 队列 队列的概念及结构 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表。队列遵…

【大模型之Graph RAG系列之二】对比传统RAG技术中使用的向量搜索技术,知识图谱有哪些优缺点?

向量搜索和知识图谱是两项用于改善搜索体验的重要技术。结合这两种技术形成的Graph RAG可以进一步提高搜索的准确性和上下文相关性。本文将深入对比向量搜索和知识图谱&#xff0c;让读者快速了解这两种技术的原理及优缺点&#xff0c;以便于将来的技术决策。 向量搜索 向量搜…

电赛入门之软件stm32keil+cubemx

hal库可以帮我们一键生成许多基本配置&#xff0c;就不需要自己写了&#xff0c;用多了hal库就会发现原来用基本库的时候都过的什么苦日子&#xff08;笑 下面我们以f103c8t6&#xff0c;也就是经典的最小核心板来演示 一、配置工程 首先来新建一个工程 这里我们配置rcc和sys&…

从“技术深耕”到“品牌绽放”,解码遨游通讯的高成长路径!

在粤港澳大湾区这片充满活力的土地上&#xff0c;科技创新正以前所未有的速度推动着各行各业的发展。在这样一个充满机遇与挑战的环境中&#xff0c;遨游通讯以其在危险作业场景和应急救援场景中提供的定制化智能终端解决方案&#xff0c;脱颖而出&#xff0c;成为危急特赛道的…

golang通用后台管理系统02(RSA加密解密,登录密码加密解密)

参考&#xff1a;https://blog.csdn.net/lady_killer9/article/details/118026802 1.加密解密工具类PasswordUtil.go package utilimport ("crypto/rand""crypto/rsa""crypto/x509""encoding/pem""fmt""log"&qu…