摸鱼时写的小游戏,看看你能玩到第几关?

3,088 阅读5分钟

前言

刷视频时在某平台看到的的一款小游戏,有点想玩但又不想买,所以发动了传统艺能 —— 仿写一个。

原版示例:

a2.gif

复刻版:

a3.gif

欢迎各位来玩:

游戏地址 源码

游戏介绍

问题区通过滚动的方式不停地展示题目,等滚动完一定数量的题目后,解锁答题区,开始依次填写答案,在回答完一题后问题区继续向后滚动。

是不是感觉很有意思呢😎

需求分析

首先,这是个非常小的纯前端项目,实现主要功能即可。

因为没有玩过原版的游戏,所以只能靠自己设计需求。

我设计了两种游戏模式

  • 闯关模式:生成一定数量的题目,设置闯关要求的正确率。用户需要回答完所有题目,最后计算正确率是否达标。
  • 无尽模式:不断地生成题目,设置最大错误量。用户可以不停地不停地答题,直到错误数量超出要求,游戏结束,最后计算用户得分。每题一分。

此外还需要要求游戏中用的参数都可自定义,所以需要做一个自定义的页面,根据需要自定义游戏难度。

溯算.png

项目搭建

Vite + Vue3 + TS + Tailwind 一梭子走起。

使用 vitesse-lite 直接生成项目目录,将目录简单改造一下,替换一下 unocss -> Tailwind(这纯粹属于个人习惯)。

由于不需要后台交互,也没打算做的太过复杂,所以一切从简。

界面设计

对于一个小前端来说,审美问题真的是一个很大的挑战,不过好在项目并不复杂,只需要设计三个页面。

虽然要简单设计,但也不能太过潦草,选择了一些渐变色做背景,从大佬的这个网站COPY了一些动画效果。整个界面还算说得过去。

a4.gif

至于那些奇奇怪怪的配色则是我随便选的😜

开始开发

1. 生成题目

首先制定好生成一组题目所需要用到的参数,运算规则使用最为简单的加减乘除,其余参数则是根据不同模式的需要来制定。

/**
 * 运算规则
 */
export type IMethods = '+' | '-' | '*' | '/'

/**
 * 题目生成的条件
 *
 * type: => ’normal‘ 闯关模式, ’endless‘ 无尽模式, ’diy‘ 自定义
 */
export interface INormalOptions {
  type: 'normal'
  level: number
  range: number // 取值范围
  preNum: number // 前置题目数量
  methods: IMethods[]
  accuracy: number // 正确率 取百分比
  questionNum: number // 题目数量
}

export interface IEndlessOptions {
  type: 'endless'
  errNumber: number // 错误数量
  preNum: number
  methods: IMethods[]
}

export interface IDiyOptions {
  type: 'diy'
  methods: IMethods[]
  range: number
  preNum: number
  successType: 'normal' | 'endless' // 按照闯关模式的通关条件, 按照无尽模式的通关条件
  accuracy?: number // 准确率
  questionNum?: number // 题目数量
  errNumber?: number // 错误数量
}

export type ICreateQuestionOptions = INormalOptions | IEndlessOptions | IDiyOptions

在游戏界面,根据路由携带的参数调用generate来生成一定数量的题目,将题目追加到题目数组中。根据题目数量进行循环,每次都通过随机的方式选取运算规则。

/**
   * 创建 num 数量的题目
   * @param num
   */
  const generate = (options: ICreateQuestionOptions, num: number) => {
    const _questionList = []
    const baseIndex = questionList.value.length

    for (let index = 0; index < num; index++) {
      const fn = getMethod(options.methods)
      let _range = 0
      /**
       * 如果有预设的范围,按照预设
       */
      if ('range' in options) {
        _range = options.range
      } else {
        /**
         * 没有预设范围,’endless‘模式 | 'diy'模式-’endless‘条件
         *
         * +、- 为20
         * *、/ 为15
         */
        if (fn === '+' || fn === '-') {
          _range = 20
        } else {
          _range = 15
        }
      }

      const question = createQuestion[fn](_range, baseIndex + index)

      _questionList.push(question)
    }

    questionList.value.push(..._questionList)

    return _questionList
  }

对于不同的运算方式要做出不同的处理来保证题目的难度。

  • 加法:从运算范围内随便算两个数字相加就行
  • 减法:与加法同理,但要保证答案是个正数或0
  • 乘法:乘除都不受运算范围的控制,因为太大的数也不好算(至少我没那个脑子去算),这里最大只取到9
  • 除法:同样是为了保证题目的运算方便,显示随机取两个数进行乘法运算,将结果与其中一个数字作为问题,另一个数字作为答案。
/**
 * 创建题目
 */
export const createQuestion: Record<IMethods, (range: number, i: number) => IQuestion> = {
  '+': (range, i) => {
    const a = getRoundNum(range - 1)
    const b = getRoundNum(range - a)

    const answer = a + b

    return { a, b, fn: '+', answer, i }
  },
  '-': (range, i) => {
    let a = getRoundNum(range)
    let b = getRoundNum(range)

    /**
     * 如果a小于b,交换位置
     * 保证答案为正数
     */
    if (a < b) {
      [a, b] = [b, a]
    }

    const answer = a - b

    return { a, b, fn: '-', answer, i }
  },
  /**
   * 不受运算范围的限制
   * @param _
   * @param i
   */
  '*': (_, i) => {
    const a = getRoundNum(9)
    const b = getRoundNum(9)
    const answer = a * b

    return { a, b, fn: '*', answer, i }
  },
  '/': (_, i) => {
    const b = getRoundNum(9)
    const answer = getRoundNum(9)
    const a = answer * b

    return { a, b, fn: '/', answer, i }
  },
}

2. 用户输入

使用系统自带的键盘来玩游戏体验十分糟糕,所以在屏幕上做了一个九宫格的小键盘,只需要 0-9 以及 删除提交

image.png

使用Grid画个键盘还是很简单的。之后是绑定键盘事件、点击事件,这里计划做到PC端与移动端通用,所以对两端的监听事件分别做了处理。并在最后都汇总到统一的提交事件中。

/**
 * 用户的答案成绩记录
 */
export const answerRecord = ref<boolean[]>([])

 /**
* 当前的答案
*/
const curAnswer = ref<string[]>([])
  
/**
* 按钮操作
* @param key
*/
const handleCurAnswer = async(key: string) => {
    /**
    * 最大输入为3位数
    */
    if (curAnswer.value.length > 2 && key !== 'Enter' && key !== 'Backspace')
      return

    if ((key === 'Enter' || key === 'Backspace') && curAnswer.value.length === 0) {
      return
    }

    /**
    * 提交当前答案
    */
    const submitCurAnswer = async() => {
       /**
       * 获取答题结果
       */
      const result = await getSubmitResult(Number(showCurAnswer.value), answerIndex.value)

      answerRecord.value.push(result)
      answerIndex.value += 1
      curAnswer.value = []

      nextTick(() => {
        submitEnd(answerRecord.value)
      })
    }

    switch (key) {
      case 'Backspace':
        curAnswer.value.pop()
        break
      case 'Enter':
        submitCurAnswer()
        break
      default:
        curAnswer.value.push(key)
        break
    }
|

3. 游戏结束

因为两种游戏模式的结束条件不同,所以每次提交当前答案时都要判断游戏是否结束

/**
* 判断游戏是否结束
*/
const isGameOver = (list: boolean[]) => {
   if (scoreType.value === 'percentage') {
     if (list.length === allQuestionLength.value) {
       return true
     }
   } else {
     if (errNumber.value > (playOptions.value as IEndlessOptions).errNumber) {
       return true
     }
   }

   return false
}

在游戏确认结束后,计算出分数与结果。

   /**
   * 计算得分
   */
   const computedScore = (list: boolean[]) => {
    const trueNum = list.filter(Boolean).length
    const allNum = list.length

    const options: IResultOptions = {
      type: scoreType.value,
      num: 0,
      result: false,
    }

    if (scoreType.value === 'percentage') {
      /**
     * 闯关模式
     * 计算分数,是否通过
     */
      const num = Math.floor(trueNum / allNum * 100)

      options.result = num >= (playOptions.value as INormalOptions).accuracy
      options.num = num
    } else {
      /**
       * 无尽模式
       * 给出分数即可
       */
      options.num = trueNum
    }

    return options
  }

image.png

结语

OK,以上就是我在公司摸鱼时写的小游戏,虽然项目很小,但玩的时候也挺有意思。欢迎各位来体验呀。

游戏地址

源码

最后不要脸的求个star,谢谢!