鸿蒙开发案例:巧算24点

【引言】
巧算24点是一个经典的数学游戏,其规则简单而富有挑战性:玩家需利用给定的四个数字,通过加、减、乘、除运算,使得计算结果等于24。本文将深入分析一款基于鸿蒙系统的巧算24点游戏的实现代码,并重点介绍其中所使用的算法及其工作原理。
【开发环境】
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.814
工程版本:API 12
【算法分析】
1、递归搜索算法
递归搜索算法是一种用来穷举所有可能性的算法。在巧算24点游戏中,我们需要通过递归地尝试所有可能的运算组合,来寻找能够使四个数字的运算结果等于24的表达式。
• 在递归过程中,每次选择两个数字进行运算;
• 如果当前只留下一个数字,并且这个数字接近于24(在一定的误差范围内),则认为找到了一个解;
• 否则,继续对剩下的数字进行递归搜索;
• 对于减法和除法,还需要考虑运算顺序,因此需要额外处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | searchSolutions(currentNumbers: number[], pathExpression: string) { if ( this .solutions.length > 0) return ; if (currentNumbers.length === 1) { if (Math.abs(currentNumbers[0] - 24) < this .accuracyThreshold) { this .solutions.push(pathExpression); } return ; } for ( let i = 0; i < currentNumbers.length - 1; i++) { for ( let j = i + 1; j < currentNumbers.length; j++) { const tempNumbers = this .removeNumberFromArray(currentNumbers, i, j); for ( let k = 0; k < 4; k++) { let tempPath = pathExpression.length > 0 ? `${pathExpression}, ` : '' ; tempPath += `(${currentNumbers[i]} ${ this .getOperationSymbol(k)} ${currentNumbers[j]})`; tempNumbers.push( this .operations[k](currentNumbers[i], currentNumbers[j])); this .searchSolutions(tempNumbers, tempPath); tempNumbers.pop(); if (k === 2 || k === 3) { let tempPathSwapped = pathExpression.length > 0 ? `${pathExpression}, ` : '' ; tempPathSwapped += `(${currentNumbers[j]} ${ this .getOperationSymbol(k)} ${currentNumbers[i]})`; tempNumbers.push( this .operations[k](currentNumbers[j], currentNumbers[i])); this .searchSolutions(tempNumbers, tempPathSwapped); tempNumbers.pop(); } } } } } |
2、最大公约数算法
最大公约数算法用于简化分数表达式,确保分数处于最简形式。
• 迭代方式:不断交换两个数的位置,直至其中一个数变为0,此时另一个数即为最大公约数;
• 递归方式:如果b不为0,则递归调用自身,参数为b和a对b取模的结果,否则返回a。
迭代方式:
1 2 3 4 5 6 7 8 | calculateIterativeGcd(a: number, b: number): number { while (b !== 0) { let temp = b; b = a % b; a = temp; } return a; } |
递归方式:
1 2 3 | findGreatestCommonDivisor(a: number, b: number): number { return b === 0 ? a : this .findGreatestCommonDivisor(b, a % b); } |
3、连分数逼近算法
连分数逼近算法用于将一个小数转换成分数形式,适用于显示计算结果。
• 使用连分数逼近的方法,不断提取整数部分,并用其构建分数;
• 直到逼近的小数与原始小数相差小于某个容差或达到了最大迭代次数为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | convertToFraction(decimal: number): string { let tolerance = 1.; let maxIterations = 1000; let iterationCount = 0; let currentDecimal = decimal; let pNumerator = 0, pDenominator = 1; let qNumerator = 1, qDenominator = 0; do { let integerPart = Math.floor(currentDecimal); let temp = pNumerator; pNumerator = integerPart * pNumerator + pDenominator; pDenominator = temp; temp = qNumerator; qNumerator = integerPart * qNumerator + qDenominator; qDenominator = temp; currentDecimal = 1 / (currentDecimal - integerPart); iterationCount++; } while (Math.abs(decimal - pNumerator / qNumerator) > decimal * tolerance && iterationCount < maxIterations); ... } |
【完整代码】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | import { promptAction } from '@kit.ArkUI' // 导入用于提示用户的工具包 @ObservedV2 // 装饰器,使类成为可观察对象 class Cell { // 定义一个Cell类,代表游戏中的一个单元格 @Trace value: number // 使用装饰器标记value属性,使其成为追踪属性 @Trace displayValue: string // 同上,用于显示的值 @Trace isVisible: boolean // 同上,判断是否可见 @Trace xPosition: number // 同上,x坐标位置 @Trace yPosition: number // 同上,y坐标位置 columnIndex: number // 列索引 rowIndex: number // 行索引 constructor(rowIndex: number, columnIndex: number) { // 构造函数 this .rowIndex = rowIndex // 设置行索引 this .columnIndex = columnIndex // 设置列索引 this .xPosition = 0 // 初始化x坐标位置 this .yPosition = 0 // 初始化y坐标位置 this .value = 0 // 初始化数值 this .displayValue = '' // 初始化显示值 this .isVisible = true // 初始化可见性 } setDefaultValue(value: number) { // 设置单元格的默认值 this .value = value // 设置数值 this .displayValue = `${value}` // 设置显示值 this .isVisible = true // 设置为可见 } performOperation(otherCell: Cell, operationName: string) { // 执行与其他单元格的操作 switch (operationName) { // 根据操作名称进行不同的运算 case "加" : // 如果是加法 this .value = otherCell.value + this .value // 计算新值 break // 结束case块 case "减" : // 如果是减法 this .value = otherCell.value - this .value // 计算新值 break // 结束case块 case "乘" : // 如果是乘法 this .value = otherCell.value * this .value // 计算新值 break // 结束case块 case "除" : // 如果是除法 if ( this .value === 0) { // 检查除数是否为0 promptAction.showToast({ message: '除数不能为0' , bottom: 400 }) // 提示错误信息 return false // 返回false,表示操作无效 } this .value = otherCell.value / this .value // 计算新值 break // 结束case块 } otherCell.isVisible = false // 隐藏参与运算的另一个单元格 this .displayValue = `${ this .value >= 0 ? '' : '-' }${ this .convertToFraction(Math.abs( this .value))}` // 更新显示值 return true // 返回true,表示操作成功 } findGreatestCommonDivisor(a: number, b: number): number { // 计算两个数的最大公约数 return b === 0 ? a : this .findGreatestCommonDivisor(b, a % b) // 使用递归算法求最大公约数 } convertToFraction(decimal: number): string { // 将小数转换为分数形式 let tolerance = 1.0E-6 // 设置容差值 let maxIterations = 1000 // 设置最大迭代次数 let pNumerator = 1 // 分子初始化 let pDenominator = 0 // 分母初始化 let qNumerator = 0 // 辅助变量 let qDenominator = 1 // 辅助变量 let currentDecimal = decimal // 当前处理的小数 let iterationCount = 0 // 迭代计数 do { // 执行直到满足条件 let integerPart = Math.floor(currentDecimal) // 取整部分 let temp = pNumerator // 临时保存分子 pNumerator = integerPart * pNumerator + pDenominator // 更新分子 pDenominator = temp // 更新分母 temp = qNumerator // 临时保存辅助变量 qNumerator = integerPart * qNumerator + qDenominator // 更新辅助变量 qDenominator = temp // 更新辅助变量 currentDecimal = 1 / (currentDecimal - integerPart) // 更新小数部分 iterationCount++ // 增加迭代计数 } while (Math.abs(decimal - pNumerator / qNumerator) > decimal * tolerance && iterationCount < maxIterations) // 继续迭代直到达到容差或最大迭代次数 if (iterationCount >= maxIterations) { // 如果达到最大迭代次数 return `${decimal}` // 返回原小数 } let gcdValue = this .calculateIterativeGcd(pNumerator, qNumerator) // 计算分子和分母的最大公约数 let reducedNumerator = pNumerator / gcdValue // 化简后的分子 let reducedDenominator = qNumerator / gcdValue // 化简后的分母 return `${reducedNumerator}${reducedDenominator !== 1 ? '/' + reducedDenominator : '' }` // 返回化简后的分数形式 } calculateIterativeGcd(a: number, b: number): number { // 使用迭代方式计算两个数的最大公约数 while (b !== 0) { // 当b不为0时继续 let temp = b // 临时保存b b = a % b // 更新b a = temp // 更新a } return a // 返回最大公约数 } } class JudgePointSolution { // 定义JudgePointSolution类,用于寻找24点游戏的解 solutions: string[] = [] // 存储找到的解 accuracyThreshold = Math.pow(10, -6) // 设置精度阈值 operations = [ // 定义四种基本运算 (a: number, b: number) => a + b, // 加法 (a: number, b: number) => a * b, // 乘法 (a: number, b: number) => a - b, // 减法 (a: number, b: number) => a / b, // 除法 ] searchSolutions(currentNumbers: number[], pathExpression: string) { // 查找解的递归方法 if ( this .solutions.length > 0) { // 如果已经找到解,则返回 return } if (currentNumbers.length === 1) { // 如果只剩下一个数 if (Math.abs(currentNumbers[0] - 24) < this .accuracyThreshold) { // 如果该数等于24(在阈值范围内) this .solutions.push(pathExpression) // 将路径表达式作为解加入数组 } return // 结束递归 } for ( let i = 0; i < currentNumbers.length - 1; i++) { // 对所有数进行两两组合 for ( let j = i + 1; j < currentNumbers.length; j++) { // 对所有数进行两两组合 const tempNumbers = this .removeNumberFromArray(currentNumbers, i, j) // 创建新的数组,移除当前两个数 for ( let k = 0; k < 4; k++) { // 对四种运算分别尝试 let tempPath = pathExpression.length > 0 ? `${pathExpression}, ` : '' // 格式化路径表达式 tempPath += `(${currentNumbers[i]} ${ this .getOperationSymbol(k)} ${currentNumbers[j]})` // 添加当前运算表达式到路径 tempNumbers.push( this .operations[k](currentNumbers[i], currentNumbers[j])) // 计算结果并加入临时数组 this .searchSolutions(tempNumbers, tempPath) // 递归查找解 tempNumbers.pop() // 移除最后一个加入的结果 if (k === 2 || k === 3) { // 如果是减法或除法 let tempPathSwapped = pathExpression.length > 0 ? `${pathExpression}, ` : '' // 格式化路径表达式 tempPathSwapped += `(${currentNumbers[j]} ${ this .getOperationSymbol(k)} ${currentNumbers[i]})` // 添加当前运算表达式到路径 tempNumbers.push( this .operations[k](currentNumbers[j], currentNumbers[i])) // 计算结果并加入临时数组 this .searchSolutions(tempNumbers, tempPathSwapped) // 递归查找解 tempNumbers.pop() // 移除最后一个加入的结果 } } } } } find24Solutions(numbers: number[]): string[] { // 查找所有可能的解 this .solutions = [] // 清空解数组 this .searchSolutions(numbers, '' ) // 开始查找 return this .solutions // 返回解数组 } getOperationSymbol(index: number): string { // 获取运算符号 const symbols = [ '+' , '*' , '-' , '/' ] // 定义符号数组 return symbols[index] // 返回对应的符号 } removeNumberFromArray(array: number[], index1: number, index2: number): number[] { // 从数组中移除指定位置的元素 const newArray: number[] = [] // 新数组 for ( let k = 0; k < array.length; k++) { // 遍历原始数组 if (k !== index1 && k !== index2) { // 如果不是需要移除的位置 newArray.push(array[k]) // 将元素加入新数组 } } return newArray // 返回新数组 } } @Entry @Component struct GameIndex { @State randomNumbers: number[] = [] // 用于存储随机生成的游戏数字 @State symbols: string[] = [ "加" , "减" , "乘" , "除" ] // 存储游戏中可用的运算符号字符串数组 @State cells: Cell[] = [ // 存储游戏中的单元格实例数组 new Cell(0, 0), // 创建位于第0行第0列的单元格 new Cell(0, 1), // 创建位于第0行第1列的单元格 new Cell(1, 0), // 创建位于第1行第0列的单元格 new Cell(1, 1) // 创建位于第1行第1列的单元格 ] @State selectedNumberIndex: number = -1 // 存储选中的数字单元格的索引,默认为-1表示未选择 @State selectedSymbolIndex: number = -1 // 存储选中的运算符号的索引,默认为-1表示未选择 @State showSolution: boolean = false // 控制是否显示游戏的解决方案,默认为不显示 cellWidth: number = 250 // 单个单元格的宽度 cellMargin: number = 15 // 单元格之间的间距 judgePoint24Util: JudgePointSolution = new JudgePointSolution() // 创建一个JudgePointSolution类的实例,用于寻找游戏的解 isShowAnim: boolean = false //单元格是否正在移动,若移动中禁止操作以防闪退 aboutToAppear(): void { this .resetGame() } resetGame() { this .randomNumbers = [] for ( let i = 0; i < this .cells.length; i++) { let randomValue = Math.floor(Math.random() * 13) + 1 this .cells[i].setDefaultValue(randomValue) this .randomNumbers.push(randomValue) } this .selectedNumberIndex = -1 this .selectedSymbolIndex = -1 this .showSolution = false let solutions = this .judgePoint24Util.find24Solutions( this .randomNumbers) console.info(`【${solutions}】`) if (solutions.length === 0) { console.info(`无解,重新循环`) this .resetGame() } } build() { Column({ space: 20 }) { // 显示/隐藏 解决方案 Text(`${ this .judgePoint24Util.find24Solutions( this .randomNumbers)}`) .fontSize(20) .fontColor(Color.White) .backgroundColor( "#ffa101" ) .visibility( this .showSolution ? Visibility.Visible : Visibility.Hidden) .padding(10) .borderRadius(10) // 数字 Row() { Flex({ wrap: FlexWrap.Wrap }) { ForEach( this .cells, (cell: Cell, index: number) => { Text(`${cell.displayValue}`) .fontSize(`${ this .cellWidth / 3}lpx`) .width(`${ this .cellWidth}lpx`) .height(`${ this .cellWidth}lpx`) .fontColor(cell !== this .cells[ this .selectedNumberIndex] ? "#ffffff" : "#fe4b00" ) .backgroundColor(cell !== this .cells[ this .selectedNumberIndex] ? "#ffa101" : "#fddf4b" ) .borderRadius(`${ this .cellMargin}lpx`) .margin(`${ this .cellMargin}lpx`) .textAlign(TextAlign.Center) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) .visibility(cell.isVisible ? Visibility.Visible : Visibility.Hidden) .translate({ x: `${cell.xPosition}lpx`, y: `${cell.yPosition}lpx` }) .onClick(() => { if ( this .selectedNumberIndex === -1) { this .selectedNumberIndex = index } else if ( this .selectedNumberIndex === index) { this .selectedNumberIndex = -1 } else if ( this .selectedSymbolIndex === -1) { console.info(`未选择操作符,仅改变选中位置`) this .selectedNumberIndex = index } else { if ( this .isShowAnim) { return } this .isShowAnim = true animateToImmediately({ duration: 300, onFinish: () => { this .cells[ this .selectedNumberIndex].xPosition = 0 // 动画结束后位置归0 this .cells[ this .selectedNumberIndex].yPosition = 0 // 动画结束后位置归0 this .cells[index].performOperation( this .cells[ this .selectedNumberIndex], this .symbols[ this .selectedSymbolIndex] ) this .selectedNumberIndex = -1 this .selectedSymbolIndex = -1 // 统计结果 let countVisibleCells: number = 0 for ( let i = 0; i < this .cells.length; i++) { if ( this .cells[i].isVisible) { countVisibleCells++ } } if (countVisibleCells === 1) { // 当前是最后一个 promptAction.showDialog({ title: '游戏结束' , message: `${ this .cells[index].value === 24 ? '【胜利】' : '【失败】' }`, buttons: [{ text: '重新开始' , color: '#ffa500' }] }).then(() => { this .resetGame() }) } this .isShowAnim = false }, }, () => { let temp = this .cellWidth + this .cellMargin // 要移动的单元格距离 let movingCell: Cell = this .cells[ this .selectedNumberIndex] movingCell.xPosition = (cell.columnIndex - movingCell.columnIndex) * temp movingCell.yPosition = (cell.rowIndex - movingCell.rowIndex) * temp }) } }) }) }.width(`${ this .cellWidth * 2 + this .cellMargin * 4}lpx`) }.width( '100%' ).justifyContent(FlexAlign.Center) // 操作符 Row() { Flex({ wrap: FlexWrap.Wrap }) { ForEach( this .symbols, (symbol: string, index: number) => { Text(`${symbol}`) .fontSize(`${ this .cellWidth / 4}lpx`) .width(`${ this .cellWidth / 2}lpx`) .height(`${ this .cellWidth / 2}lpx`) .fontColor( this .selectedSymbolIndex !== index ? "#c16cf9" : "#fcfeff" ) .backgroundColor( this .selectedSymbolIndex !== index ? Color.Transparent : "#c16cf9" ) .borderRadius(`${ this .cellMargin}lpx`) .margin(`${ this .cellMargin / 2}lpx`) .textAlign(TextAlign.Center) .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) .onClick(() => { if ( this .selectedSymbolIndex === index) { this .selectedSymbolIndex = -1 } else { this .selectedSymbolIndex = index } }) }) }.width(`${ this .cellWidth * 2 + this .cellMargin * 4}lpx`) }.width( '100%' ).justifyContent(FlexAlign.Center) // 重新开始 / 解决方案 Row() { Button( '重新开始' ).clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }).onClick(() => { this .resetGame() }) Button( '解决方案' ).clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }).onClick(() => { this .showSolution = ! this .showSolution }) }.width( '100%' ).justifyContent(FlexAlign.SpaceEvenly) } .width( '100%' ).height( '100%' ) .backgroundColor( "#0d1015" ) .padding(20) } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了