kos/web/src/assets/scripts/GameMap.js

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