294 lines
11 KiB
JavaScript
294 lines
11 KiB
JavaScript
// 在 AcGameObject.js 里使用的是 export class ,因此这里需要使用 {} 括起来引用;如果是 export default 则不需要用括号括起来
|
|
import { AcGameObject } from "./AcGameObject";
|
|
import { Snake } from "./Snake";
|
|
// 导入墙组件
|
|
import { Wall } from "./Wall";
|
|
|
|
// 导出定义的 GameMap 游戏地图类
|
|
export class GameMap extends AcGameObject {
|
|
// 构造函数参数: ctx 画布; parent 画布的父元素,用来动态修改画布的长宽
|
|
constructor(ctx, parent, store) {
|
|
// super() 用于先执行基类的构造函数
|
|
super();
|
|
|
|
// 存下 ctx 和 parent
|
|
this.ctx = ctx;
|
|
this.parent = parent;
|
|
this.store = store;
|
|
// 存下每个格子的绝对距离
|
|
this.L = 0;
|
|
|
|
/* // 定义棋盘格的行数和列数(前端生成地图时使用)
|
|
// 行数和列数不同时设置为偶数或者不同时设置为奇数,可以避免AB两蛇同时进入同一个格子,避免因此对优势者不公平
|
|
this.rows = 13;
|
|
this.cols = 14;
|
|
// 绘制棋盘内部区域的障碍物(墙)的数量(前端生成地图时使用)
|
|
this.inner_walls_count = 20; */
|
|
|
|
this.rows = this.store.state.pk.rows;
|
|
this.cols = this.store.state.pk.cols;
|
|
this.inner_walls_count = this.store.state.pk.inner_walls_count;
|
|
|
|
// 存储所有的墙
|
|
// 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
|
|
this.walls = [];
|
|
|
|
// 创建蛇对象数组
|
|
this.snakes = [
|
|
// 注意这里的对象生成方式和传参方式
|
|
new Snake({ id: 0, color: "#4876ec", r: this.rows - 2, c: 1 }, this),
|
|
new Snake({ id: 1, color: "#f94848", r: 1, c: this.cols - 2 }, this),
|
|
];
|
|
}
|
|
|
|
/*
|
|
// 判断函数:判断角色路径是否联通。传入参数:g数组,起点和终点的横纵坐标(前端生成地图时使用)
|
|
check_connectivity(g, sx, sy, tx, ty) {
|
|
// 当起点坐标和中点坐标一致时,判断联通,直接返回
|
|
if (sx == tx && sy == ty) return true;
|
|
g[sx][sy] = true;
|
|
|
|
// 定义四方向偏移量
|
|
let dx = [-1, 0, 1, 0],
|
|
dy = [0, 1, 0, -1];
|
|
// 枚举上下左右四个方向,求当前点下一个相邻点的坐标
|
|
for (let i = 0; i < 4; i++) {
|
|
let x = sx + dx[i],
|
|
y = sy + dy[i];
|
|
// 判断是否撞墙,如果没有撞墙,且可以搜到终点的话,返回 true ,否则返回 false
|
|
if (!g[x][y] == true && this.check_connectivity(g, x, y, tx, ty))
|
|
return true;
|
|
}
|
|
// 搜不到终点,返回 false
|
|
return false;
|
|
}
|
|
*/
|
|
|
|
// 创建障碍物(后端生成地图版本)
|
|
create_walls() {
|
|
// 取出后端生成后传到前端 store 中的 game_map
|
|
const g = this.store.state.pk.game_map;
|
|
|
|
// 枚举数组,将 g[r][c] == true 的部分绘制出来
|
|
// 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作
|
|
for (let r = 0; r < this.rows; r++) {
|
|
for (let c = 0; c < this.cols; c++) {
|
|
if (g[r][c]) {
|
|
// 将每个新生成的 Wall 对象 push 存入 walls 数组中
|
|
this.walls.push(new Wall(r, c, this));
|
|
}
|
|
}
|
|
}
|
|
// 绘制成功则 return turn
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
// 创建障碍物(前端计算地图版本)
|
|
create_wall() {
|
|
// 创建一个墙格进行测试
|
|
// new Wall(0,0,this);
|
|
|
|
// 开一个布尔数组,有墙为 true
|
|
// 一开始先将所有墙初始化为 false
|
|
const g = [];
|
|
for (let r = 0; r < this.rows; r++) {
|
|
g[r] = [];
|
|
for (let c = 0; c < this.cols; c++) {
|
|
g[r][c] = false;
|
|
}
|
|
}
|
|
|
|
// 给左右加上墙
|
|
for (let r = 0; r < this.rows; r++) {
|
|
g[r][0] = g[r][this.cols - 1] = true;
|
|
}
|
|
|
|
// 给上下加上墙
|
|
for (let c = 0; c < this.cols; c++) {
|
|
g[0][c] = g[this.rows - 1][c] = true;
|
|
}
|
|
|
|
// 创建内部随机障碍物
|
|
// 因为每次计算都会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2
|
|
for (let i = 0; i < this.inner_walls_count / 2; i++) {
|
|
// 避免位置重复:重复 1000 次,只要找到了就禁止随机
|
|
for (let j = 0; j < 1000; j++) {
|
|
let r = parseInt(Math.random() * this.rows);
|
|
let c = parseInt(Math.random() * this.cols);
|
|
|
|
// 主对角线对称 g[r][c] 和 g[c][r] 完成两种联合判断
|
|
// 当此位置已经有障碍物了,则重新计算下一个位置
|
|
// if(g[r][c] || g[c][r]) continue;
|
|
// 解决中心对称问题,需要将注释代码修改为下一行
|
|
if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;
|
|
|
|
// 将计算求得的随机障碍物的位置置为 true ,以对该位置进行绘制
|
|
// g[r][c] 和 g[c][r] 的坐标在对角线位置会重合,会被绘制为一个障碍物
|
|
// g[r][c] || g[c][r] = true;
|
|
// 解决中心对称问题,需要将注释代码修改为下一行
|
|
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
|
|
|
|
// 1000 次中,规定数量的内部障碍物已经够了之后就 break 掉
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 避免内部障碍物覆盖掉左下角和右上角的角色出发点
|
|
g[this.rows - 2][1] = g[1][this.cols - 2] = false;
|
|
|
|
// 保证两个对角角色的运动区域是联通的
|
|
// 检测联通需要把 g[][] 传过去给 check_connectivity() 函数进行判断,传过去之前需要把当前 g[][] 状态复制一份,避免当前数据被修改掉
|
|
// 深度复制方法:先转换数据为 JSON ,再把 JSON 解析出来
|
|
const copy_g = JSON.parse(JSON.stringify(g));
|
|
// 检测到不连通,则直接在生成对象之前 return false 退出函数
|
|
if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2))
|
|
return false;
|
|
|
|
// 枚举数组,将 g[r][c] == true 的部分绘制出来
|
|
// 如果上一步连通性检测失败,则退出 this.create_wall() 函数,本步骤不再执行生成新对象的操作
|
|
for (let r = 0; r < this.rows; r++) {
|
|
for (let c = 0; c < this.cols; c++) {
|
|
if (g[r][c]) {
|
|
// 将每个新生成的 Wall 对象 push 存入 walls 数组中
|
|
this.walls.push(new Wall(r, c, this));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 绘制成功则 return turn
|
|
return true;
|
|
}
|
|
*/
|
|
|
|
// 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
|
|
add_listening_events() {
|
|
// 聚焦到获取输入的画布页面
|
|
this.ctx.canvas.focus();
|
|
|
|
// 先取出两条蛇对象
|
|
const [snake0, snake1] = this.snakes;
|
|
|
|
// 获取用户信息:绑定 keydown 事件
|
|
this.ctx.canvas.addEventListener("keydown", (e) => {
|
|
// 定义 snake0 的键盘绑定事件
|
|
if (e.key === "w") snake0.set_direction(0);
|
|
else if (e.key === "d") snake0.set_direction(1);
|
|
else if (e.key === "s") snake0.set_direction(2);
|
|
else if (e.key === "a") snake0.set_direction(3);
|
|
// 定义 snake1 的键盘绑定事件
|
|
else if (e.key === "ArrowUp") snake1.set_direction(0);
|
|
else if (e.key === "ArrowRight") snake1.set_direction(1);
|
|
else if (e.key === "ArrowDown") snake1.set_direction(2);
|
|
else if (e.key === "ArrowLeft") snake1.set_direction(3);
|
|
});
|
|
}
|
|
|
|
start() {
|
|
// 开始时调用一次创建墙的函数
|
|
// 循环 1000 次,如果成功创建则 break ,否则继续循环创建(前端生成地图时使用这一条)
|
|
// for (let i = 0; i < 1000; i++) if (this.create_walls()) break;
|
|
|
|
// 使用后端生成地图时,这里只需要调用一次就好
|
|
this.create_walls();
|
|
|
|
// 开始时启动监听方法
|
|
this.add_listening_events();
|
|
}
|
|
|
|
// 每一帧都更新一下小正方格的边长
|
|
update_size() {
|
|
// 计算当前帧每个格子的宽度, parseInt 取整是为了避免渲染出的格子之间出现小空隙
|
|
this.L = parseInt(
|
|
Math.min(
|
|
this.parent.clientWidth / this.cols,
|
|
this.parent.clientHeight / this.rows
|
|
)
|
|
);
|
|
// 计算当前画布的宽度
|
|
this.ctx.canvas.width = this.L * this.cols;
|
|
// 计算当前画布的高度
|
|
this.ctx.canvas.height = this.L * this.rows;
|
|
}
|
|
|
|
// 判断两条蛇是否准保好进入下一回合
|
|
check_ready() {
|
|
for (const snake of this.snakes) {
|
|
// 判断蛇的状态,当状态不为静止,表示蛇上一步骤没有执行终止,返回 false 表示未准备好进入下一回合
|
|
if (snake.status !== "idle") return false;
|
|
// 判断蛇下一步指令的方向,如果为 -1 ,表示蛇目前没有获取到方向指令,返回 false 表示未准备好进入下一回合
|
|
if (snake.direction === -1) return false;
|
|
}
|
|
|
|
// 当上面的条件判断都满足进入下一回合的条件时,返回 true 表示准备好进入下一步
|
|
return true;
|
|
}
|
|
|
|
// 让两条蛇进入下一回合
|
|
next_step() {
|
|
for (const snake of this.snakes) {
|
|
snake.next_step();
|
|
}
|
|
}
|
|
|
|
// 增加碰撞检测:检测目标位置是否合法:没有撞到蛇的身体和墙
|
|
check_valid(cell) {
|
|
// 碰墙检测
|
|
for (const wall of this.walls) {
|
|
if (wall.r === cell.r && wall.c === cell.c) return false;
|
|
}
|
|
|
|
// 蛇身碰撞检测
|
|
for (const snake of this.snakes) {
|
|
// 先特判蛇尾碰撞情况
|
|
let k = snake.cells.length;
|
|
// 当蛇尾可以前进(蛇尾可以前进说明此回合蛇尾没有增加)的时候,蛇尾不要判断
|
|
if (!snake.check_tail_increasing()) {
|
|
k--;
|
|
}
|
|
// 判断蛇身现有结点是否碰撞
|
|
for (let i = 0; i < k; i++) {
|
|
if (snake.cells[i].r === cell.r && snake.cells[i].c === cell.c)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 没有撞到障碍物时, return true
|
|
return true;
|
|
}
|
|
|
|
update() {
|
|
this.update_size();
|
|
// 当两条蛇都准备好进入下一回合后
|
|
if (this.check_ready()) {
|
|
this.next_step();
|
|
}
|
|
// 每次更新都重新执行渲染
|
|
this.render();
|
|
}
|
|
|
|
// 渲染函数,把当前的游戏对象绘制到地图上
|
|
render() {
|
|
// b47226 棕色 aad751 浅绿 a2d048 深绿
|
|
// this.ctx.fillStyle = 'green';
|
|
// this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
|
|
|
// 定义偶数格even、奇数格odd的颜色
|
|
const color_even = "#aad751",
|
|
color_odd = "#a2d048";
|
|
|
|
for (let r = 0; r < this.rows; r++) {
|
|
for (let c = 0; c < this.cols; c++) {
|
|
// 当列标加行标: r + c 是偶数时,选取偶数颜色,否则选取奇数颜色。
|
|
if ((r + c) % 2 == 0) {
|
|
this.ctx.fillStyle = color_even;
|
|
} else {
|
|
this.ctx.fillStyle = color_odd;
|
|
}
|
|
// 绘制小方格:起始坐标x,起始坐标y,水平边长,竖直边长
|
|
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
|
|
}
|
|
}
|
|
}
|
|
}
|