软工导论-2-记忆广度(微信小程序)的设计开发实装

记忆广度-微信小程序开发-个人总结

博客班级 https://edu.cnblogs.com/campus/zjcsxy/SE2020
作业要求 https://edu.cnblogs.com/campus/zjcsxy/SE2020/homework/11633
班级 计算机1803
姓名 章一砚
学号 31801146

记忆广度是什么:游戏设计与规则介绍

  • 规则:给出一个用方块拼接成的图形,每个方块中有一个数字,图形显示的时间根据难度设定,三个难度等级设定的时间分别为5秒,10秒,15秒,图形显示结束后打乱方块顺序,要求被测者将方块拖回原来的位置。
    结束条件:连续做错3题或完成全部的题目;
    每题时间限制:90秒;
    评分:做对1题得1分,共18题,最高分为18分
    难度一:由五个数字方块组成的图形
    难度二:由六个数字方块组成的图形
    难度三:由七个数字方块组成的图形
  • GIF演示

游戏设计 - 界面:

  • 棋盘:用于摆放方格。我们设计一个 4 × 4 的棋盘,用于摆放方格。(之后,我们会使用“棋子”这个称呼来代替方格)
  • 方格(棋子):方格应该有三种状态【空白格子、棋盘底色格子、有数字的格子】
  • 方格(棋子)拼接成的图形:应该是在这个 4 × 4 的棋盘中随机生成的一个图形
  • 方格(棋子)待选区:将之前在棋盘中出现过的数字方格打乱顺序,放置在棋盘下方的待选区。
  • 状态栏:包括
    • 当前游戏状态【等待开始、请记忆棋盘、请拖动棋子(等待提交答案)】
    • 当前游戏进度【第 1 / 6 关】
    • 当前环节倒计时【还剩 ... 秒】
  • 按钮组:至少有一个 “提交答案” 的按钮,用于在等待用户拖动棋子的倒计时中,让用户主动提交答案,进入下一个环节。

游戏设计 - 数据:

  • 棋盘大小 board_size:设定为 4 × 4 即可,没有变化的必要
  • 棋子个数 level:有数字的、需要记忆的棋子个数 —— 暂定为 5 6 7 个,需要支持使用方便的参数进行动态的修改。
  • 记忆耗时 level_time:对于不同的棋子个数,记忆的时间暂定为 —— 5 10 15 秒,需要支持使用方便的参数进行动态的修改。
  • 棋子拖动耗时常量即可:最多不超过10个棋子需要被拖动。为保险起见,设定30秒用于等待拖动,绰绰有余。
  • 棋盘中的每个棋子是什么:
    • 使用一个 Number[] board_num[16] 来保存。
    • 数组元素为 -1 的,应该是空白棋子,不然,就是数字棋子。
  • 棋子本身的相关信息:
    • 棋子是哪个数字对应的棋子——数字本身
    • 棋子在本次游戏中,被放在了棋盘的哪个位置
      • 可以从 board_num[16] 中遍历查得。
      • 但是为了方便,不如再设计一个数组 chess_index[level]
        • 第 i 个 棋子,在本局中的初始位置是 chess_index[i]
    • 棋子现在被放在了哪个位置 chess_nowAt[16]
      • -1 表示棋子正处于待选区
      • 否则,表示棋子正处于棋盘的某个位置
      • 如果 chess_index[i] = chess_nowAt[chess_index[i]] 则棋子位置符合答案要求
  • 棋子动态拖动需要的相关信息:
    • chess_start[16] 用于保存棋子初始位置。 chess_move[16] 用于保存棋子位移。
    • 棋子初始位置:
      • 这个初始位置指的是棋子在待选区时的初始位置。
    • 棋子位移:
      • 这个位移指的是棋子当前位置和在待选区的位置的相对位移。
    • 棋子当前位置:
      • 棋子初始位置 + 棋子位移 = 棋子当前位置
    • pos_table[16] 用于保存棋盘中的16个格子各自的坐标位置。
      • 当需要获取棋子当前位置是否是棋盘中的某个格子时
      • 遍历 pos_table[16],与棋子当前位置进行比较
      • 误差允许则返回匹配到的位置在 pos_table[16] 中的索引值
      • 否则,返回-1
    • chess_zindex[16] 用于保存棋子的CSS属性 z-index 的值
      • 我们希望棋子在被拖动时,应该是 z-index 值最大的,不会被别的UI元素覆盖遮挡住

各功能 具体实现 复盘

生成一个随机的棋盘

初级需求

  • 这个棋盘应该是一个长为 16 的数字数组
  • 这个数组中 有 level 个不是 -1 的数
    • 不是 -1 的数,包括 1,2,3,4,5,6,7,8,9
    • 注意,不含 0
  • 棋盘应该是随机的

初级需求的实现

原地随机一个数组
// 有很多种写法,反正,写起来不难
/**
 * 打乱一个数组(原地打乱)
 * @param {Number[]} arr 需要被打乱的数组
 * @returns {Number[]} 被打乱后的数组的引用(其实和被传入的引用是同一个)
 */
function randArr(arr) {
  for (var i = 0; i < arr.length; i++) {
    var iRand = parseInt(arr.length * Math.random());
    [arr[i], arr[iRand]] = [arr[iRand], arr[i]];
  }
  return arr;
}
获取初级需求的一个随机棋盘
/**
 * 建立随机的初始棋盘
 * @param {Number} n 有 n 个有效的格子
 * @param {Number} size 总共有 size 个格子
 * @returns {Number[]} 包含`n`个不同的正整数和`size-n`个`-1`的一维数组
 */
function get_Random_board(n, size) {
  let base = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  randArr(base);
  let out = base.slice(0, n);
  for (let i = n; i < size; i++) {
    out.push(-1);
  }
  randArr(out);
  // prettyBoard(out); /* 这一步是进阶需求:整理优化棋盘 */
  return out;
}

进阶需求

  • 按初级需求中的方式生成的棋盘,是随机的。
  • 这样的随机同时会导致棋子在棋盘上的散乱分布。
  • 作为记忆广度的棋盘,我们不希望棋子过于散乱分布。
  • 我们希望棋子尽量是一定程度上连续的。

进阶需求的实现

检查棋子图形是否足够连续
  • 检查棋子连续性,其实也就是,检查棋子是不是”连通图“
  • 用 BFS/DFS 遍历之即可
/**
 * 检查棋盘数组是否足够聚拢
 * @param {Number[]} board 被检查的棋盘数组
 * @returns {Boolean} 返回`True` or `False`
 */
function isOK(board) {
  let start = -1;
  let totalCnt = 0;
  let stack = []; /* 预备查看的索引值 栈 */
  let isVisited = []; /* -1:无需访问 0:已经访问 >0:需要访问还未访问 */
  let step_cnt = 0;

  /* 遍历出所有合法点个数 */
  for (let i = 0; i < board.length; i++) {
    if (board[i] != -1) {
      totalCnt++;
      /* 保存第一个合法点的下标 */
      if (start == -1) {
        start = i;
      }
    }
    isVisited.push(board[i]);
  }
  stack.push(start);
  do {
    let x = stack.pop(); /* 取出并保存本次局部搜索的起点索引 */
    let tars = [
      x - 1,
      x + 1,
      x - 4,
      x + 4,
    ]; /* 本次局部搜索四个方向上的索引 */

    tars.forEach((tar) => {
      /* 如果坐标未越界 */
      if (tar >= 0 || tar < board.length) {
        /* 当前节点待遍历 */
        if (isVisited[tar] > 0) {
          isVisited[tar] = 0; /* 标记为已经遍历 */
          step_cnt++; /* 有效值计数器自增 */
          stack.push(tar); /* 当前位置标记为下一个起点 */
        } /* 否则(≤0),无需访问 */
      }
    });
  } while (stack.length > 0);
  return step_cnt == totalCnt;
}
调整棋盘使图形聚拢
  • 将整个 4 × 4 理解为 中间的一个 2 × 2 和外面的一圈
  • 遍历外面的一圈棋子,尝试让他们向内移动一格
    • 如果外圈棋子内的格子是有棋子的,则不移动
    • 否则,向内移动一格
/**
 * 调整`i`号位的棋子
 * @param {Number[]} 待调整的棋盘数组
 * @param {Number} i 这一步想调整的棋子的索引值
 */
function step(board, i) {
  /* 如果 位置上没有数字 or i是在中间的那四个格子(索引分别是 5 6 9 10) */
  if (board[i] == -1 || [5, 6, 9, 10].indexOf(i) > -1) {
    /* 则什么都不做 */
    return;
  }
  /* 如果 是第1列 */
  if (i % 4 == 0) {
    /* 则 向右 挪动一格 */
    if (board[i + 1] == -1) {
      [board[i], board[i + 1]] = [board[i + 1], board[i]];
    }
  } else if (i % 4 == 3) {
    /* 如果 是第4列 */
    /* 则 向左 挪动一格 */
    if (board[i - 1] == -1) {
      [board[i], board[i - 1]] = [board[i - 1], board[i]];
    }
  } else if (i / 4 < 1) {
    /* 如果 是第1行 */
    /* 则 向下 挪动一格 */
    if (board[i + 4] == -1) {
      [board[i], board[i + 4]] = [board[i + 4], board[i]];
    }
  } else if (i / 4 >= 3) {
    /* 如果 是第4行 */
    /* 则 向上 挪动一格 */
    if (board[i - 4] == -1) {
      [board[i], board[i - 4]] = [board[i - 4], board[i]];
    }
  }
}
组合棋盘聚拢和连通性检查
  • 遍历并尝试移动一遍以后,再次检查棋盘是否足够连通
  • 如果还没有,那就再遍历一轮并尝试移动
  • 如此,往复几轮,基本上能让棋盘成为连通图
  • 既使还没完全连通,也问题不大了(不是过于散乱了)
/**
 * 整理棋盘,使棋子聚拢向中间
 * @param {Number[]} board 需要被聚拢的棋盘数组(原地聚拢)
 */
function prettyBoard(board) {
  for (let k = 0; k < 3; k++) {
    if (!isOK(board)) { for (let i = 0; i < board.length; i++) { step(board, i); } } 
    else { break; }
  }
}

棋盘大小及棋子大小的动态设定

  • 这个棋盘的棋子个数是固定 4 × 4 的
  • 因此,棋子大小只与屏幕大小有关
  • 棋盘是方形的,长度和宽度相同
  • 按照团队协作规范,棋盘宽度设定为屏幕宽度的80%
/* 获取设备屏幕大小 */
let that = this;
wx.getSystemInfo({
  success(res) {
    that.windowWidth = res.windowWidth; /* 屏幕宽度 */
    that.windowHeight = res.windowHeight; /* 屏幕高度 */
    that.chess_size = (res.windowWidth * 0.8) / 7, /* 待选区棋子大小 */
  },
});

/* WXML 棋盘(中每个棋子的大小) */
<view style="width:calc({{(windowWidth*0.8)/4}}px);height:calc({{(windowWidth*0.8)/4}}px );" />
/* WXML 棋子(待拖拽) */
<view class="chess_drag" style="width:{{chess_size}}px;height:{{chess_size}}px" />

棋子可拖拽的实现

  • 棋子的定位由 left 和 top 实现

  • 若 left. top 的是0,则意味着棋子在一开始的位置,没动。

  • 棋子样式(WXML)属性值

    style="z-index:{{chess_zindex[item]}};left:{{chess_move[item].left}}px;top:{{chess_move[item].top}}px;"

  • WX 的拖拽事件,可以在WXML中设置属性值传递进 eventdataset

<view 
	class="chess_drag" 
	wx:for="{{chess_index}}" 
	wx:key="item"
	id="chess_{{board_num[item]}}" 
	data-who="{{item}}"
  data-i="{{index}}"
	catchtouchstart="moveStart"
  catchtouchmove="handleMove" 
	catchtouchend="moveEnd" 
  style="z-index:{{chess_zindex[item]}};left:{{chess_move[item].left}}px;top:{{chess_move[item].top}}px;">
    <image src='{{board_img_url[item]}}' style="width:{{chess_size}}px;height:{{chess_size}}px" />
</view>

棋子的整个拖拽过程分为三部分

触摸开始

catchtouchstart="moveStart"

  • 如果发现棋子是第一次被触摸(chess_start[who] 的 left 和 top 都是 0)
  • 那么获取当前触摸事件的被点击对象的位置,存储进 chess_start[who]
  • 同时,为了让被拖动的棋子元素是是浮动在上方,所以设一个大一点的 z-index 值
/**
 * 处理 触摸开始
 * @todo 初始化棋子的初始位置
 * @todo 被拖动的棋子 z-index 调高至200
 */
moveStart: function (event) {
  if (this.data.game_state != "开始拖动吧") { return; }
  let who = event.currentTarget.dataset.who;
  let chess_start = this.data.chess_start;
  let param = {};
  if (chess_start[who].left == 0 && chess_start[who].top == 0) {
    param["chess_start[" + who + "]"] = {
      /**
       * 此处,根据棋子大小做了矫正。
       * 因为 WXML 的元素定位点并不是元素中心,而是(大概)左上角的一个位置。
       * 设置了 display: inline 所以定位点是左边中间靠下一点点的一个位置。
       * 进行了宽度的一半,高度的四分之一的位置修正后,定位点就是元素中心了。
       */
      left: event.currentTarget.offsetLeft + this.data.chess_size / 2,
      top: event.currentTarget.offsetTop - this.data.chess_size / 6,
    };
  }
  param["chess_zindex[" + who + "]"] = 200;
  this.setData(param);
}

触摸移动中

catchtouchmove="handleMove"

  • 通过 event.currentTarget.dataset.who 得知现在是哪个棋子被拖动了
  • 通过 this.data.chess_start[who] 得知这个棋子一开始的初始坐标,便于后面计算位置偏移量
  • 通过 event.changedTouches[0].pageX 得知本次触摸点的 X 坐标。Y 坐标同理。
  • 检查这个坐标是否符合棋盘某个格子的要求
    • 若符合要求,则,设置位移为格子坐标减去棋子初始坐标
    • 不符合要求,则,设置位移为当前触摸坐标减去棋子初始坐标
  • 位移的更新到 chess_move[who],实际效果就体现为棋子被手指拖着到哪就到哪

触摸结束

catchtouchend="moveEnd"

  • 和之前同理,从 event 中取数据,检查坐标是否符合棋盘某个格子的要求
    • 若符合要求,则,啥也不做
    • 不符合要求,则,设置位移为 {left:0, top:0} 使棋子回到待选区
  • 拖动已经结束了,z-index 可以调回来,从200改回100

棋盘各个格子的初始位置的读取

  • 用微信提供的查询API,获取每个格子节点
  • 遍历得到的节点序列,并获取其位置,然后存储
  • 最重要的是
    • 因为棋盘有一个入场加载动画是微信自带的
    • 为了保证获取到的棋盘是真的棋盘
    • 而不是动画中飞到半路的棋盘
    • 我们设置一个两秒的延时
    • 延时以后再进行查询
/* 延迟两秒后再更新棋盘位置表,避免出现错误 */
setTimeout(() => {
  let query = wx.createSelectorQuery();
  query.selectAll('.chess_map > .chess_box').boundingClientRect();
  query.exec((res) => {
    let pos_table = [];
    res[0].forEach((e) => {
      pos_table[parseInt(e.id)] = {
        // left: e.left,
        // right: e.right,
        // top: e.top,
        // bottom: e.bottom,
        // centerX: (e.left + e.right) / 2,
        // centerY: (e.top + e.bottom) / 2,
        left: (e.left + e.right) / 2,
        top: (e.top + e.bottom) / 2,
      }
    });
    // console.log(pos_table);
    that.setData({
      pos_table: pos_table
    });
  });
}, 2000);

最后总结

最后实装的主要内容就在这里了。其实中间还有一些失败的技术探索,各种半成品。真正花了大量时间的就是这些爬坑的过程。

posted @ 2020-12-18 16:09  章一砚  阅读(128)  评论(0编辑  收藏  举报