鸿蒙NEXT开发案例:巧算24点

zhongcx
发布于 2024-12-1 09:41
浏览
0收藏

鸿蒙NEXT开发案例:巧算24点-鸿蒙开发者社区

【引言】
巧算24点是一个经典的数学游戏,其规则简单而富有挑战性:玩家需利用给定的四个数字,通过加、减、乘、除运算,使得计算结果等于24。本文将深入分析一款基于鸿蒙系统的巧算24点游戏的实现代码,并重点介绍其中所使用的算法及其工作原理。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【算法分析】
1、递归搜索算法
递归搜索算法是一种用来穷举所有可能性的算法。在巧算24点游戏中,我们需要通过递归地尝试所有可能的运算组合,来寻找能够使四个数字的运算结果等于24的表达式。
• 在递归过程中,每次选择两个数字进行运算;
• 如果当前只留下一个数字,并且这个数字接近于24(在一定的误差范围内),则认为找到了一个解;
• 否则,继续对剩下的数字进行递归搜索;
• 对于减法和除法,还需要考虑运算顺序,因此需要额外处理。

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。
迭代方式:

calculateIterativeGcd(a: number, b: number): number {
  while (b !== 0) {
    let temp = b;
    b = a % b;
    a = temp;
  }
  return a;
}

递归方式:

findGreatestCommonDivisor(a: number, b: number): number {
  return b === 0 ? a : this.findGreatestCommonDivisor(b, a % b);
}

3、连分数逼近算法
连分数逼近算法用于将一个小数转换成分数形式,适用于显示计算结果。
• 使用连分数逼近的方法,不断提取整数部分,并用其构建分数;
• 直到逼近的小数与原始小数相差小于某个容差或达到了最大迭代次数为止。

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);
  ...
}

【完整代码】

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 // 生成1到13之间的随机数
      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) // 查找24点的解
    console.info(`【${solutions}】`) // 打印找到的解
    if (solutions.length === 0) { // 如果没有解
      console.info(`无解,重新循环`) // 打印无解信息
      this.resetGame() // 重新开始游戏
    }
  }

  build() { // 构建UI界面
    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 // 更新x坐标
                    movingCell.yPosition = (cell.rowIndex - movingCell.rowIndex) * temp // 更新y坐标
                  })
                }
              })
          })
        }.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) // 设置内边距
  }
}

分类
标签
收藏
回复
举报
回复
    相关推荐