【引言】

巧算24点是一个经典的数学游戏,其规则简单而富有挑战性:玩家需利用给定的四个数字,通过加、减、乘、除运算,使得计算结果等于24。本文将深入分析一款基于鸿蒙系统的巧算24点游戏的实现代码,并重点介绍其中所使用的算法及其工作原理。

【开发环境】

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.814

工程版本:API 12

【算法分析】

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
      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)

  }
}

zhongcx
1 声望3 粉丝