改变坐标系

GameMap.jscreate_walls 里修改创造的墙的对称的位置

image-20240808215339291

为了使canvas能够读取键盘操作

GameMap.vue中的canvas标签中添加属性tabindex="0"

image-20240808215508507

GameMap变黑了就表示聚焦了

然后就是漫长的前端js编写过程

GameMap.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import { AcGameObjects } from "./AcGameObjects";
import { Wall } from "./Wall";
import { Snake } from "./Snake";

export class GameMap extends AcGameObjects {
constructor(ctx, parent) {
super();

this.ctx = ctx;
this.parent = parent;
this.L = 0;

this.rows = 13;
this.cols = 13;

this.walls = [];
this.inner_walls_count = 20;

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

check_ready() { //判断两条蛇是否都准备好下一回合了
for (const snake of this.snakes) {
if (snake.status !== "idle")
return false;
if (snake.direction === -1)
return false;
}
return true;
}

check_connectivity(g, sx, sy, ex, ey) {
if (sx == ex && sy == ey) return true;

g[sx][sy] = true;
const dx = [1, 0, -1, 0];
const dy = [0, 1, 0, -1];

for (let i = 0; i < 4; i++) {
let x = dx[i] + sx, y = dy[i] + sy;
if (!g[x][y] && this.check_connectivity(g, x, y, ex, ey))
return true;
}

return false;
}

create_walls() {
let 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;
}

for (let i = 0; i < this.inner_walls_count / 2; i++) {
for (let j = 0; j < 100; j++) {
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if (g[r][c])
continue;
if ((r == 1 && c == this.cols - 2) || (r == this.rows - 2 && c == 1))
continue;
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
break;
}
}

const copy_g = JSON.parse(JSON.stringify(g));
if (!this.check_connectivity(copy_g, 1, this.cols - 2, this.rows - 2, 1))
return false;

for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if (g[r][c])
this.walls.push(new Wall(r, c, this));
}
}

return true;
}


add_listening_events() {
this.ctx.canvas.focus();

const [snake0, snake1] = this.snakes;
this.ctx.canvas.addEventListener("keydown", e => {
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);
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() {
for (let i = 0; i < 100; i++)
if (this.create_walls())
break;

this.add_listening_events();
}

update_size() {
this.L = parseInt(Math.min(this.parent.clientWidth / this.rows, this.parent.clientHeight / this.cols));
this.ctx.canvas.width = this.L * this.cols;
this.ctx.canvas.height = this.L * this.rows;
}

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

update() {
this.update_size();
if (this.check_ready()) {
this.next_step();
}
this.render();
}

render() {
const color_even = "#AAD781", color_odd = "#A2D149";
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
if ((r + c) % 2 == 0) {
this.ctx.fillStyle = color_even;
} else {
this.ctx.fillStyle = color_odd;
}
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}
}

Snake.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { AcGameObjects } from "./AcGameObjects";
import { Cell } from "./Cell";

export class Snake extends AcGameObjects {
constructor(info, gamemap) {
super();
this.id = info.id;
this.color = info.color;
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; //存放蛇的身体,cells[0]存放蛇头
this.next_cell = null; //下一步的目标位置

this.speed = 3;
this.direction = -1; //-1表示没有指令, 0123表示上右下左
this.status = "idle"; //idle表示静止,move表示移动,die表示死亡

this.dr = [-1, 0, 1, 0];
this.dc = [0, 1, 0, -1];

this.step = 0;

this.eps = 0.1; //误差

this.eye_direction = 0;
if (this.id === 1) this.eye_direction = 2;

this.eye_dx = [ // 蛇眼睛不同方向的x的偏移量
[-1, 1],
[1, 1],
[1, -1],
[-1, -1],
];
this.eye_dy = [ // 蛇眼睛不同方向的y的偏移量
[-1, -1],
[-1, 1],
[1, 1],
[1, -1],
]

}

start() {

}

check_tail_increasing() {
if (this.step <= 10) return true;
if (this.step % 3 === 1) return true;
return false;
}

//写一个统一的接口来调整方向
set_direction(d) {
this.direction = d;
}

next_step() { //将蛇的状态变为走下一步
const d = this.direction;
this.eye_direction = d;
this.next_cell = new Cell(this.cells[0].r + this.dr[d], this.cells[0].c + this.dc[d]);
this.direction = -1; //清空操作
this.status = "move";
this.step++;

const k = this.cells.length;
for (let i = k; i > 0; i--) {
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
}

if (!this.gamemap.check_valid(this.next_cell)) {
this.status = "die";
}
}

update_move() {
const move_distance = this.speed * this.timedelta / 1000; //没两帧之间走过的距离
const dx = this.next_cell.x - this.cells[0].x;
const dy = this.next_cell.y - this.cells[0].y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < this.eps) {
this.cells[0] = this.next_cell;
this.next_cell = null;
this.status = "idle";

if (!this.check_tail_increasing()) { // 蛇不变长
this.cells.pop();
}
} else {
this.cells[0].x += move_distance * dx / distance;
this.cells[0].y += move_distance * dy / distance;

if (!this.check_tail_increasing()) {
const k = this.cells.length;
const tail = this.cells[k - 1], tail_taget = this.cells[k - 2];
const tail_dx = tail_taget.x - tail.x;
const tail_dy = tail_taget.y - tail.y;
tail.x += move_distance * tail_dx / distance;
tail.y += move_distance * tail_dy / distance;
}
}
}

update() {
if (this.status === "move") {
this.update_move();
}
this.render();
}

render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;

ctx.fillStyle = this.color;
if (this.status === "die") {
ctx.fillStyle = "white";
}
for (const cell of this.cells) {
ctx.beginPath();
ctx.arc(cell.x * L, cell.y * L, L / 2 * 0.8, 0, Math.PI * 2);
ctx.fill();
}

for (let i = 1; i < this.cells.length; i++) {
const a = this.cells[i - 1], b = this.cells[i];
if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y - b.y) < this.eps) {
continue;
}
if (Math.abs(a.x - b.x) < this.eps) {
ctx.fillRect((a.x - 0.4) * L, Math.min(a.y, b.y) * L, L * 0.8, Math.abs(a.y - b.y) * L);
}
if (Math.abs(a.y - b.y) < this.eps) {
ctx.fillRect(Math.min(a.x, b.x) * L, (a.y - 0.4) * L, Math.abs(a.x - b.x) * L, L * 0.8);
}
}

ctx.fillStyle = "black";
for (let i = 0; i < 2; i++) {
const eye_x = (this.cells[0].x + this.eye_dx[this.eye_direction][i] * 0.15) * L;
const eye_y = (this.cells[0].y + this.eye_dy[this.eye_direction][i] * 0.15) * L;

ctx.beginPath();
ctx.arc(eye_x, eye_y, L * 0.05, 0, Math.PI * 2);
ctx.fill();
}

}
}