10.1 实现AcApp端(上)

master
barney 2 years ago
parent 2c381678af
commit d4e0391f83
  1. 98
      acapp/package-lock.json
  2. 6
      acapp/package.json
  3. 79
      acapp/src/App.vue
  4. BIN
      acapp/src/assets/images/background.png
  5. BIN
      acapp/src/assets/logo.png
  6. 50
      acapp/src/assets/scripts/AcGameObject.js
  7. 9
      acapp/src/assets/scripts/Cell.js
  8. 155
      acapp/src/assets/scripts/GameMap.js
  9. 168
      acapp/src/assets/scripts/Snake.js
  10. 24
      acapp/src/assets/scripts/Wall.js
  11. 49
      acapp/src/components/ContentField.vue
  12. 38
      acapp/src/components/GameMap.vue
  13. 58
      acapp/src/components/HelloWorld.vue
  14. 199
      acapp/src/components/MatchGround.vue
  15. 24
      acapp/src/components/PlayGround.vue
  16. 78
      acapp/src/components/ResultBoard.vue
  17. 22
      acapp/src/components/UserInfo.vue
  18. 2
      acapp/src/main.js
  19. 8
      acapp/src/store/index.js
  20. 52
      acapp/src/store/pk.js
  21. 29
      acapp/src/store/record.js
  22. 18
      acapp/src/store/router.js
  23. 94
      acapp/src/store/user.js
  24. 78
      acapp/src/views/MenuView.vue
  25. 99
      acapp/src/views/pk/PkIndexView.vue
  26. 133
      acapp/src/views/ranklist/RanklistIndexView.vue
  27. 19
      acapp/src/views/record/RecordContentView.vue
  28. 197
      acapp/src/views/record/RecordIndexView.vue
  29. 279
      acapp/src/views/user/bot/UserBotIndexView.vue
  30. 9
      acapp/vue.config.js

@ -8,8 +8,13 @@
"name": "acapp",
"version": "0.1.0",
"dependencies": {
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.0",
"core-js": "^3.8.3",
"jquery": "^3.6.1",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vue3-ace-editor": "^2.2.2",
"vuex": "^4.0.0"
},
"devDependencies": {
@ -17,6 +22,7 @@
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
@ -1924,6 +1930,11 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.4.tgz",
@ -3108,6 +3119,11 @@
"node": ">= 0.6"
}
},
"node_modules/ace-builds": {
"version": "1.10.1",
"resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.10.1.tgz",
"integrity": "sha512-w8Xj6lZUtOYAquVYvdpZhb0GxXrZ+qpVfgj5LP2FwUbXE8fPrCmfu86FjwOiSphx/8PMbXXVldFLD2+RIXayyA=="
},
"node_modules/acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.7.1.tgz",
@ -3562,6 +3578,14 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true
},
"node_modules/bootstrap": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.2.0.tgz",
"integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==",
"peerDependencies": {
"@popperjs/core": "^2.11.5"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -6566,6 +6590,11 @@
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jquery": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.6.1.tgz",
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
},
"node_modules/js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
@ -8816,6 +8845,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz",
@ -10120,6 +10154,17 @@
"node": ">=8"
}
},
"node_modules/vue-router": {
"version": "4.1.5",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.5.tgz",
"integrity": "sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ==",
"dependencies": {
"@vue/devtools-api": "^6.1.4"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -10142,6 +10187,16 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"node_modules/vue3-ace-editor": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/vue3-ace-editor/-/vue3-ace-editor-2.2.2.tgz",
"integrity": "sha512-fZ6OWosbU+odLrtrcGC/536QjCigujYJB0Hf6/tBp+ef/ohTadwQAqyBlVzOmvrmzZyubphpV9zkaZcx5Fuivw==",
"dependencies": {
"ace-builds": "^1.4.13",
"resize-observer-polyfill": "^1.5.1",
"vue": "^3.2.26"
}
},
"node_modules/vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",
@ -12191,6 +12246,11 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
},
"@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.4.tgz",
@ -13212,6 +13272,11 @@
"negotiator": "0.6.3"
}
},
"ace-builds": {
"version": "1.10.1",
"resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.10.1.tgz",
"integrity": "sha512-w8Xj6lZUtOYAquVYvdpZhb0GxXrZ+qpVfgj5LP2FwUbXE8fPrCmfu86FjwOiSphx/8PMbXXVldFLD2+RIXayyA=="
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.7.1.tgz",
@ -13572,6 +13637,11 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"dev": true
},
"bootstrap": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.2.0.tgz",
"integrity": "sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -15959,6 +16029,11 @@
"@sideway/pinpoint": "^2.0.0"
}
},
"jquery": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.6.1.tgz",
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
},
"js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz",
@ -17672,6 +17747,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz",
@ -18729,6 +18809,14 @@
}
}
},
"vue-router": {
"version": "4.1.5",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.5.tgz",
"integrity": "sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ==",
"requires": {
"@vue/devtools-api": "^6.1.4"
}
},
"vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@ -18753,6 +18841,16 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue3-ace-editor": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/vue3-ace-editor/-/vue3-ace-editor-2.2.2.tgz",
"integrity": "sha512-fZ6OWosbU+odLrtrcGC/536QjCigujYJB0Hf6/tBp+ef/ohTadwQAqyBlVzOmvrmzZyubphpV9zkaZcx5Fuivw==",
"requires": {
"ace-builds": "^1.4.13",
"resize-observer-polyfill": "^1.5.1",
"vue": "^3.2.26"
}
},
"vuex": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.0.2.tgz",

@ -8,8 +8,13 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.0",
"core-js": "^3.8.3",
"jquery": "^3.6.1",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vue3-ace-editor": "^2.2.2",
"vuex": "^4.0.0"
},
"devDependencies": {
@ -17,6 +22,7 @@
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",

@ -1,26 +1,75 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<div class="game-body">
<MenuView v-if="$store.state.router.router_name === 'menu'" />
<PkIndexView v-else-if="$store.state.router.router_name === 'pk'" />
<RanklistIndexView
v-else-if="$store.state.router.router_name === 'ranklist'"
/>
<RecordIndexView v-else-if="$store.state.router.router_name === 'record'" />
<RecordContentView
v-else-if="$store.state.router.router_name === 'record_content'"
/>
<UserBotIndexView
v-else-if="$store.state.router.router_name === 'user_bot'"
/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import { useStore } from "vuex";
import MenuView from "./views/MenuView.vue";
import PkIndexView from "./views/pk/PkIndexView.vue";
import RanklistIndexView from "./views/ranklist/RanklistIndexView.vue";
import RecordIndexView from "@/views/record/RecordIndexView.vue";
import RecordContentView from "@/views/record/RecordContentView.vue";
import UserBotIndexView from "@/views/user/bot/UserBotIndexView.vue";
export default {
name: 'App',
components: {
HelloWorld
}
}
MenuView,
PkIndexView,
RanklistIndexView,
RecordIndexView,
RecordContentView,
UserBotIndexView,
},
setup() {
const store = useStore();
// localStoragetoken
const jwt_token =
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0YTMyOTdhYzE0YmY0ZDVkYmU2ODAxZDVkNTk3ODhiYyIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjYyMTAwNCwiZXhwIjoxNjYzODMwNjA0fQ.rKZG8n3U_U50Aql25nB2k27cZWtxSpE4__a8bnSP3Fs";
if (jwt_token) {
store.commit("updateToken", jwt_token);
store.dispatch("getInfo", {
//
success() {
store.commit("updatePullingInfo", false);
},
error() {
store.commit("updatePullingInfo", false);
},
});
} else {
store.commit("updatePullingInfo", false);
}
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
<style scoped>
body {
margin: 0;
}
div.game-body {
background-image: url("@/assets/images/background.png");
background-size: cover;
width: 100%;
height: 100%;
}
div.window {
width: 100vw;
height: 100vh;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,50 @@
const AC_GAME_OBJECTS = [];
export class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this); // 数组添加元素
this.timedelta = 0; // 注意单位是毫秒,转化为秒需要除于1000
this.has_called_start = false;
}
start() { // 只执行一次
}
update() { // 每一帧执行一次,除了第一帧之外
}
on_destroy() { // 删除之前执行
}
destroy() {
this.on_destroy();
for (let i in AC_GAME_OBJECTS) { // 遍历数组下标
const obj = AC_GAME_OBJECTS[i];
if (obj === this) { // 注意是三个等号
AC_GAME_OBJECTS.splice(i); // 从数组中移除元素
break;
}
}
}
}
let last_timestamp; // 上一次执行的时刻
const step = timestamp => {
for (let obj of AC_GAME_OBJECTS) { // 遍历数组的值
if (!obj.has_called_start) { // 如果对象没有被调用
obj.has_called_start = true;
obj.start();
} else {
obj.timedelta = timestamp - last_timestamp; // 获得当前帧和上一帧的时间差
obj.update();
}
}
last_timestamp = timestamp; // 将当前时间更新为上一帧执行时刻
requestAnimationFrame(step) // 递归调用该函数
}
requestAnimationFrame(step)

@ -0,0 +1,9 @@
export class Cell{
constructor(r,c) { // 传入蛇的坐标(r,c)
this.r = r;
this.c = c;
this.x = c + 0.5; // 将蛇的左上角坐标(r,c)转化为画布上的圆心坐标(x,y)
this.y = r + 0.5;
}
}

@ -0,0 +1,155 @@
import { AcGameObject } from "./AcGameObject"; // 导入js的export class
import { Snake } from "./Snake";
import { Wall } from "./Wall";
export class GameMap extends AcGameObject {
constructor(ctx, parent, store) {
super(); // 继承类一直要先调用父类的构造函数
this.ctx = ctx;
this.parent = parent;
this.L = 0;
this.rows = 13;
this.cols = 14;
this.store = store;
this.inner_walls_count = 20; // 障碍物的数量(最大建议80)
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),
];
}
create_walls() { // 判断是否生成有效的地图
const g = this.store.state.pk.gamemap;
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));
}
}
}
}
add_listening_events() { // 绑定监听事件
if (this.store.state.record.is_record === true) { // 播放录像
let k = 0;
const a_steps = this.store.state.record.a_steps;
const b_steps = this.store.state.record.b_steps;
const loser = this.store.state.record.record_loser;
const [snake0,snake1] = this.snakes;
let interval_id = setInterval(() => {
if (k >= a_steps.length - 1) { // 不回放最后一步,最后一步直接展示结果
if (loser == "all" || loser == "A") {
snake0.status = "die";
}
if (loser == "all" || loser == "B") {
snake1.status = "die";
}
clearInterval(interval_id);
}else {
snake0.set_direction(parseInt(a_steps[k]));
snake1.set_direction(parseInt(b_steps[k]));
}
k++;
},300);
}else {
this.ctx.canvas.focus(); // 聚焦
// 用wasd控制方向
this.ctx.canvas.addEventListener("keydown", e => {
let d = -1;
if (e.key === 'w') d = 0;
else if (e.key === 'd') d = 1;
else if (e.key === 's') d = 2;
else if (e.key === 'a') d = 3;
this.store.state.pk.socket.send(JSON.stringify({
event: "move",
direction: d,
}));
});
}
}
start() {
this.create_walls();
this.add_listening_events();
}
update_size() { // 每过一帧重新生成新的地图尺寸
// 取整数
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;
}
update() { // 更新地图:每隔一帧都要重新渲染
this.update_size(); // 更新地图大小
if (this.check_ready()) {
this.next_step(); // 进入下一回合
}
this.render(); // 重新渲染
}
check_ready() { // 判断两条蛇是否准备好进行下一步
for (const snake of this.snakes) {
if (snake.status !== "idle") return false;
if (snake.direction === -1) return false;
}
return true;
}
next_step() { // 让两条蛇进入下一回合
for (const snake of this.snakes) {
snake.next_step();
}
}
check_vaild(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;
}
render() {
const color_even = "#AAD751", 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);
}
}
}
}

@ -0,0 +1,168 @@
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";
export class Snake extends AcGameObject {
constructor(info,gamemap) { // 传入蛇的信息和地图
super();
this.id = info.id; // 哪条蛇
this.color = info.color; // 蛇的颜色
this.gamemap = gamemap; // 方便调用地图的有关参数
this.cells = [new Cell(info.r,info.c)] // cells存放蛇的坐标,cells[0]存放蛇头
this.speed = 5; // 定义蛇的速度,每秒走5个格子
this.direction = -1; // -1表示没有指令,0,1,2,3分别表示左右下左
this.status = "idle"; // idle表示静止,move表示移动,die表示死亡
this.next_cell = null; // 下一步的目标位置
// 0,1,2,3:上右下左
this.dr = [-1,0,1,0]; // 行方向的偏移量
this.dc = [0,1,0,-1]; // 列方向的偏移量
this.step = 0; // 蛇当前的回合数
this.eps = 1e-2; // 允许误差为0.01
this.eye_direction = 0; // 左下角的蛇的眼睛朝上
if (this.id === 1) this.eye_direction = 2; // 右上角的蛇的眼睛朝下
// 定义蛇左右眼的x和y坐标的偏移量
this.eyes_dx = [
[-1,1],
[1,1],
[-1,1],
[-1,-1]
];
this.eyes_dy = [
[-1,-1],
[-1,1],
[1,1],
[-1,1]
];
}
start() { // 第一帧调用该函数
}
update() { // 更新下一帧
if (this.status === "move") { // 只有下一步的状态为移动才可以移动
this.update_move(); // 蛇进行移动
}
this.render();
}
check_tail_increasing() { // 前10步每步增加1,后面每三步增加1
if (this.step <= 10) return true;
if (this.step % 3 === 1) return true;
return false;
}
next_step() { // 将蛇的状态置为下一步
const d = this.direction;
// 更新下一步的位置
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.eye_direction = d; // 更新蛇的眼睛方向
this.step++; // 回合数加一
const k = this.cells.length;
for (let i = k; i > 0; i--) {
// 每个数组元素先后移动一位,相当于复制了一份第一个元素
// 1,2,3 => 1,1,2,3
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i-1]));
}
}
set_direction(d) { // 通过读取用户输入来设置方向
this.direction = d;
}
update_move() { // 蛇进行移动
// // 蛇向右移动5个格子
// this.cells[0].x += this.speed * this.timedelta / 1000; // 注意每一帧不一定是1s
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 {
const move_distance = this.speed * this.timedelta / 1000; //移动距离
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_target = this.cells[k-2];
const tail_dx = tail_target.x - tail.x;
const tail_dy = tail_target.y - tail.y;
tail.x += move_distance * tail_dx / distance;
tail.y += move_distance * tail_dy / distance;
}
}
}
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) {
// 矩形起始点的 x轴坐标、y轴坐标、矩形width、矩形height
// 用Math.min()是因为这两个矩阵上下关系不知道
ctx.fillRect((a.x - 0.4) * L,Math.min(a.y,b.y) * L,L*0.8,Math.abs(a.y-b.y) * L);
}
else {
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.eyes_dx[this.eye_direction][i] * 0.2) * L;
const eye_y = (this.cells[0].y + this.eyes_dy[this.eye_direction][i] * 0.2) * L;
// 画眼睛
ctx.beginPath();
ctx.arc(eye_x,eye_y,L*0.05,0,Math.PI*2);
ctx.fill();
}
}
}

@ -0,0 +1,24 @@
import { AcGameObject } from "./AcGameObject";
export class Wall extends AcGameObject {
constructor(r, c, gamemap) {
super();
this.r = r;
this.c = c;
this.gamemap = gamemap;
this.color = "#B37226"; // 障碍物的颜色
}
update() {
this.render();
}
render() {
const L = this.gamemap.L;
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
ctx.fillRect(this.c * L, this.r * L, L, L); // 将对应位置填充为障碍物
}
}

@ -0,0 +1,49 @@
<template>
<div class="content-field">
<slot></slot>
<div class="back" @click="handleClickBack">返回</div>
</div>
</template>
<script>
import { useStore } from "vuex";
export default{
setup() {
const store = useStore();
const handleClickBack = () => {
store.commit("updateRouterName","menu");
}
return {
handleClickBack,
}
},
}
</script>
<style scoped>
div.content-field {
width: 100%;
height: 100%;
}
div.back {
position: absolute;
right: 5vw;
bottom: 5vh;
cursor: pointer;
font-size: 24px;
font-weight: bold;
font-style: italic;
color: white;
user-select: none;
}
div.back:hover {
scale: 1.2;
transition: 400ms;
}
</style>

@ -0,0 +1,38 @@
<template>
<div ref="parent" class="gamemap"> <!--使用下面返回的parent-->
<!--tabindex=0获取用户的输入操作-->
<canvas ref="canvas" tabindex="0"></canvas> <!--使用下面返回的canvas-->
</div>
</template>
<script>
import { GameMap } from "@/assets/scripts/GameMap"; // jsGameMap(public)
import { ref, onMounted } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
let parent = ref(null);
let canvas = ref(null);
const store = useStore();
onMounted(() => {
store.commit("updateGameObject", new GameMap(canvas.value.getContext('2d'), parent.value,store));
});
return {
parent,
canvas
}
}
}
</script>
<style scoped>
div.gamemap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
</style>

@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

@ -0,0 +1,199 @@
<template>
<div class="matchground_field">
<div class="matchground">
<div class="matchground-head">
<div>
<div class="user_photo">
<img :src="$store.state.user.photo" alt="" />
</div>
<div class="username">
{{ $store.state.user.username }}
</div>
</div>
<div class="select-bot">
<select
class="form-select"
v-model="select_bot"
aria-label="Default select example"
>
<option value="-1">用户出战</option>
<option v-for="bot in bots" :key="bot.id" :value="bot.id">
{{ bot.title }}
</option>
</select>
</div>
<div>
<div class="user_photo">
<img :src="$store.state.pk.opponent_photo" alt="" />
</div>
<div class="username">
{{ $store.state.pk.opponent_username }}
</div>
</div>
</div>
<div class="mybtn">
<button
type="button"
v-if="is_matched"
class="match-btn"
@click="click_match_btn"
>
{{ match_btn_info }}
</button>
<button
type="button"
v-else
class="cancel-btn"
@click="click_match_btn"
>
{{ match_btn_info }}
</button>
</div>
</div>
</div>
</template>
<script>
import { ref } from "vue";
import { useStore } from "vuex";
import $ from "jquery";
export default {
setup() {
const store = useStore();
let match_btn_info = ref("开始匹配");
let is_matched = ref(true);
let bots = ref([]);
let select_bot = ref("-1");
const click_match_btn = () => {
if (match_btn_info.value === "开始匹配") {
match_btn_info.value = "取消匹配";
is_matched.value = false;
console.log(select_bot.value);
// json
store.state.pk.socket.send(
JSON.stringify({
event: "start-matching",
bot_id: select_bot.value,
})
);
} else {
match_btn_info.value = "开始匹配";
is_matched.value = true;
store.state.pk.socket.send(
JSON.stringify({
event: "stop-matching",
})
);
}
};
const refresh_bots = () => {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/bot/getlist/",
type: "GET",
headers: {
Authorization: "Bearer " + store.state.user.token, //
},
success(resp) {
// bot
bots.value = resp;
},
});
};
refresh_bots();
return {
match_btn_info,
is_matched,
click_match_btn,
bots,
select_bot,
};
},
};
</script>
<style scoped>
div.matchground_field {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
div.matchground {
width: 60%;
height: 60%;
background-color: rgba(123, 197, 212, 0.5);
display: flex;
flex-direction: column;
justify-content: space-around;
}
div.matchground-head {
display: flex;
justify-content: space-evenly;
}
img {
margin-top: 10vh;
border-radius: 50%;
width: 12vh;
}
.user_photo {
text-align: center;
}
.username {
margin-top: 30px;
font-size: 20px;
font-weight: bold;
color: black;
text-align: center;
}
.select-bot {
width: 15vw;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.select-bot > select {
font-size: 18px;
width: 20vw;
border-radius: 5px;
height: 6vh;
}
.mybtn {
text-align: center;
margin-bottom: 10vh;
}
.mybtn > .match-btn {
background: #198754;
border: none;
border-color: #198754;
font-size: 20px;
padding: 5px 5px;
border-radius: 5px;
}
.mybtn > .cancel-btn {
background: #dc3545;
border: none;
border-color: #dc3545;
font-size: 20px;
padding: 5px 5px;
border-radius: 5px;
}
</style>

@ -0,0 +1,24 @@
<template>
<div class="playground">
<GameMap />
</div>
</template>
<script>
import GameMap from './GameMap.vue' // GameMap(.vue)
export default {
components: { // 使GameMap
GameMap,
}
}
</script>
<style scoped>
/* div标签的样式 */
div.playground {
width: 60vw;
height: 70vh;
margin: 40px auto;
}
</style>

@ -0,0 +1,78 @@
<template>
<div class="result-board">
<div class="result-text" v-if="$store.state.pk.loser === 'all'">
Draw
</div>
<div class="result-text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id === parseInt($store.state.user.id)">
Lose
</div>
<div class="result-text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id == parseInt($store.state.user.id)">
Lose
</div>
<div class="result-text" v-else>
You Win
</div>
<div class="reset-btn">
<button @click="reset" class="btn btn-warning lg">再来一次!</button>
</div>
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const reset = () => {
store.commit("updateStatus","matching");
store.commit("updateLoser","none");
store.commit("updateOpponent", {
username: "我的对手",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
}
return {
reset,
}
}
}
</script>
<style scoped>
.result-board {
width: 30vw;
height: 30vh;
background-color: rgba(34, 20, 77, 0.5);
position: absolute;
top: 30vh;
left: 35vw;
}
.result-text {
font-size: 50px;
color:yellowgreen;
font-weight: bold;
text-align: center;
font-style: italic;
padding-top: 4vh;
padding-bottom: 4vh;
}
.reset-btn {
width: 100%;
display: flex;
justify-content: center;
}
</style>

@ -0,0 +1,22 @@
<template>
<div class="show">
<div class="snakeInfo" style="background-color: #4876ec;" v-if="$store.state.user.id == $store.state.pk.a_id">蓝方</div>
<div class="snakeInfo" style="background-color: #f94848;" v-if="$store.state.user.id == $store.state.pk.b_id">红方</div>
</div>
</template>
<script>
</script>
<style scoped>
.snakeInfo {
width: 50px;
height: 50px;
border-radius: 50%;
position: absolute;
top: 30vh;
left: 8vw;
text-align: center;
line-height: 50px;
}
</style>

@ -2,4 +2,4 @@ import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).use(store).mount('#app')
createApp(App).use(store).mount('#app')

@ -1,4 +1,8 @@
import { createStore } from 'vuex'
import ModuleUser from "./user"
import ModulePk from "./pk"
import ModuleRecord from "./record"
import ModuleRouter from "./router"
export default createStore({
state: {
@ -10,5 +14,9 @@ export default createStore({
actions: {
},
modules: {
user: ModuleUser,
pk: ModulePk,
record: ModuleRecord,
router: ModuleRouter,
}
})

@ -0,0 +1,52 @@
export default {
state: { // 全局变量
status: "matching", // matching表示匹配界面,playing表示对战界面
socket: null,
opponent_username: "", //对手的用户名
opponent_photo: "", // 对手的头像
gamemap: null, // 对战的地图
a_id: 0,
a_sx: 0,
a_sy: 0,
b_id: 0,
b_sx: 0,
b_sy: 0,
gameObject: null,
loser: "none" //"none","all","A","B" none表示还没有结果
},
getters: {
},
mutations: { // 用于修改全局数据
updateSocket(state, socket) {
state.socket = socket;
},
updateOpponent(state, opponent) {
state.opponent_username = opponent.username;
state.opponent_photo = opponent.photo;
},
updateStatus(state, status) {
state.status = status;
},
updateGame(state, game) {
state.gamemap = game.map;
state.a_id = game.a_id;
state.a_sx = game.a_sx;
state.a_sy = game.a_sy;
state.b_id = game.b_id;
state.b_sx = game.b_sx;
state.b_sy = game.b_sy;
},
updateGameObject(state, gameObject) {
state.gameObject = gameObject;
},
updateLoser(state, loser) {
state.loser = loser;
}
},
actions: { // 在actions中调用修改全局变量的函数
},
modules: {
}
}

@ -0,0 +1,29 @@
export default {
state: {
is_record: false,
a_steps: "",
b_steps: "",
record_loser: "",
},
getters: {
},
mutations: {
updateIsRecord(state, is_record) {
state.is_record = is_record;
},
updateSteps(state, data) {
state.a_steps = data.a_steps;
state.b_steps = data.b_steps;
},
updateRecordLoser(state,record_loser) {
state.record_loser = record_loser;
}
},
actions: {
},
modules: {
}
}

@ -0,0 +1,18 @@
export default {
state: {
router_name: "menu", // menu,pk,record,record_content,ranklist,user_bot
},
getters: {
},
mutations: {
updateRouterName(state, router_name) {
state.router_name = router_name;
},
},
actions: {
},
modules: {
}
}

@ -0,0 +1,94 @@
import $ from 'jquery'
export default {
state: { // 全局变量
id: "",
username: "",
photo: "",
token: "",
is_login: false, // 默认未登录状态
pulling_info: true, // 是否正在拉取信息
},
getters: {
},
mutations: { // 用于修改全局数据
updateUser(state,user) { // 更新用户信息
state.id = user.id;
state.username = user.username;
state.photo = user.photo;
state.is_login = user.is_login;
},
updateToken(state,token) { // 更新token
state.token = token;
},
logout(state) {
state.id = "";
state.username = "";
state.photo = "";
state.token = "";
state.is_login = false;
},
updatePullingInfo(state,pulling_info) {
state.pulling_info = pulling_info;
}
},
actions: { // 在actions中调用修改全局变量的函数
login(context,data) {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/account/token/",
type : "POST",
data: {
username: data.username,
password: data.password,
},
success(resp) {
// resp对应的就是后端的map的json格式,每个字段对应键值对中的key
// 登陆成功
if(resp.error_msg === "success"){
// 调用updateToken函数,形参中的data为resp.token
// 将登陆信息的token存入localStorage实现持久化(浏览器刷新退出登陆)
localStorage.setItem("jwt_token",resp.token);
context.commit("updateToken",resp.token);
data.success(resp);
}else {
data.error(resp);
}
},
error(resp) {
data.error(resp);
},
});
},
getInfo(context,data) {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/account/info/",
type: "GET",
headers: {
Authorization: "Bearer " + context.state.token,
},
success(resp) {
if (resp.error_msg === "success"){
// 登陆成功后,更新当前用户信息
context.commit("updateUser",{
...resp,
is_login: true,
});
data.success(resp); // 调用UserLoginView中的success()回调函数
}else {
data.error(resp); // 调用UserLoginView中的error()回调函数
}
},
error(resp) {
data.error(resp);
},
});
},
logout(context) {
// 退出时删除localStorage保存的token
localStorage.removeItem("jwt_token");
context.commit("logout"); // 退出登录
}
},
modules: {
}
}

@ -0,0 +1,78 @@
<template>
<div class="menu-field">
<div class="menu">
<div class="menu-item" @click="handlePkClick">对战</div>
<div class="menu-item" @click="handleRecordClick">对局列表</div>
<div class="menu-item" @click="handleRankListClick">排行榜</div>
<div class="menu-item" @click="handleUserBotClick">我的bot</div>
</div>
</div>
</template>
<script>
import { useStore } from "vuex";
export default {
setup() {
const store = useStore();
const handlePkClick = () => {
store.commit("updateRouterName","pk");
};
const handleRecordClick = () => {
store.commit("updateRouterName","record");
};
const handleRankListClick = () => {
store.commit("updateRouterName","ranklist");
};
const handleUserBotClick = () => {
store.commit("updateRouterName","user_bot");
};
return {
handlePkClick,
handleRecordClick,
handleRankListClick,
handleUserBotClick,
}
}
}
</script>
<style scoped>
div.menu-field {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
div.menu {
width: 25vw;
height: 30vh;
background-color: rgba(0, 0, 0, 0.3);
}
div.menu-item {
width: 100%;
height: 7.5vh;
line-height: 7.5vh;
font-size: 24px;
color: white;
text-align: center;
font-style: italic;
font-weight: bold;
cursor: pointer;
user-select: none;
}
div.menu-item:hover {
scale: 1.2;
transition: 400ms;
}
</style>

@ -0,0 +1,99 @@
<template>
<ContentField>
<PlayGround v-if="$store.state.pk.status === 'playing'" />
<MatchGround v-if="$store.state.pk.status === 'matching'" />
<ResultBoard v-if="$store.state.pk.loser !== 'none'" />
<UserInfo v-if="$store.state.pk.status === 'playing'" />
</ContentField>
</template>
<script>
import PlayGround from "../../components/PlayGround.vue";
import MatchGround from "../../components/MatchGround.vue";
import ResultBoard from "@/components/ResultBoard.vue";
import UserInfo from "@/components/UserInfo.vue";
import { onMounted, onUnmounted } from "vue"; //
import { useStore } from "vuex";
import ContentField from "@/components/ContentField.vue";
export default {
components: {
PlayGround,
MatchGround,
ResultBoard,
UserInfo,
ContentField,
},
setup() {
const store = useStore();
const socket_url = `wss://kob.bnblogs.cc/websocket/${store.state.user.token}/`;
store.commit("updateLoser", "none");
store.commit("updateIsRecord", false);
let socket = null;
// (pk)
// onMountedsetup
onMounted(() => {
store.commit("updateOpponent", {
username: "我的对手",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
// socket
socket = new WebSocket(socket_url);
socket.onopen = () => {
store.commit("updateSocket", socket);
console.log("connnected!");
};
//
socket.onmessage = (msg) => {
const data = JSON.parse(msg.data);
//
if (data.event === "start-matching") {
store.commit("updateOpponent", {
username: data.opponent_username,
photo: data.opponent_photo,
});
setTimeout(() => {
store.commit("updateStatus", "playing");
}, 200); // 0.2(穿bug)
// (id)
store.commit("updateGame", data.game);
} else if (data.event === "move") {
const game = store.state.pk.gameObject;
const [snake0, snake1] = game.snakes;
snake0.set_direction(data.a_direction);
snake1.set_direction(data.b_direction);
} else if (data.event === "result") {
const game = store.state.pk.gameObject;
const [snake0, snake1] = game.snakes;
if (data.loser == "all" || data.loser == "A") {
snake0.status = "die";
}
if (data.loser == "all" || data.loser == "B") {
snake1.status = "die";
}
store.commit("updateLoser", data.loser);
}
};
socket.onclose = () => {
console.log("disconnected!");
store.commit("updateStatus", "matching");
};
});
// pk
onUnmounted(() => {
socket.close();
});
},
};
</script>
<style scoped></style>

@ -0,0 +1,133 @@
<template>
<ContentField>
<table class="table table-striped table-hover table-sm" style="text-align: center;">
<thead>
<tr>
<th>玩家</th>
<th>积分</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id" class=align-middle>
<td>
<img :src="user.photo" alt="" class="user_photo">&nbsp;
<div class="username userA">{{user.username}}</div>
</td>
<td>
{{user.rating}}
</td>
</tr>
</tbody>
</table>
<nav aria-label="...">
<ul class="pagination" style="float: right;">
<li class="page-item" @click="click_page(-2)">
<a class="page-link" href="#">上一页</a>
</li>
<li :class="'page-item ' + page.is_active" v-for="page in pages" :key="page.number" @click="click_page(page.number)">
<a class="page-link" href="#">{{ page.number }}</a>
</li>
<li class="page-item" @click="click_page(-1)">
<a class="page-link" href="#">下一页</a>
</li>
</ul>
</nav>
</ContentField>
</template>
<script>
import ContentField from '../../components/ContentField.vue';
import { useStore } from 'vuex';
import $ from 'jquery';
import { ref } from 'vue';
export default {
components: {
ContentField
},
setup() {
const store = useStore();
let currentPage = 1;
let total_users = 0; //
let users = ref([]); //
let pages = ref([]); //
const updatePages = () => {
let max_pages = parseInt(Math.ceil(total_users / 8));
let new_pages = [];
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
if (i >= 1 && i <= max_pages) {
new_pages.push({
number: i,
is_active: i === currentPage ? "active" : "",
})
}
}
pages.value = new_pages;
}
const click_page = page => {
if (page === -2) page = currentPage - 1;
else if (page === -1) page = currentPage + 1;
let max_pages = parseInt(Math.ceil(total_users / 8));
if (page >= 1 && page <= max_pages) {
pull_page(page);
}
}
const pull_page = page => {
currentPage = page;
$.ajax({
url: "https://kob.bnblogs.cc/api/ranklist/getlist/",
type: "GET",
data: {
page
},
headers:{
"Authorization": "Bearer " + store.state.user.token, //
},
success(resp){
users.value = resp.users;
total_users = resp.users_count;
updatePages();
}
})
}
pull_page(currentPage);
return {
click_page,
users,
pages,
}
}
}
</script>
<style scoped>
.user_photo{
width: 4vh;
border-radius: 50%;
}
.username {
font-weight: bold;
font-size: 1vw;
}
.userA {
color: blue;
}
.userB {
color: red;
}
</style>

@ -0,0 +1,19 @@
<template>
<PlayGround />
</template>
<script>
import PlayGround from "../../components/PlayGround.vue";
export default {
components: {
PlayGround,
},
setup() {
}
}
</script>
<style scoped></style>

@ -0,0 +1,197 @@
<template>
<ContentField>
<table class="table table-striped table-hover table-sm align-middle" style="text-align: center;">
<thead>
<tr>
<th>player A</th>
<th>player B</th>
<th>对战结果</th>
<th>对战时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in records" :key="record.record.id" class=align-middle>
<td >
<img :src="record.a_photo" alt="" class="user_photo">&nbsp;
<div class="username userA">{{record.a_username}}</div>
</td>
<td>
<img :src="record.b_photo" alt="" class="user_photo">&nbsp;
<div class="username userB">{{record.b_username}}</div>
</td>
<td>
<div class="info">
{{record.result}}
</div>
</td>
<td>
<div class="info">
{{record.record.createTime}}
</div>
</td>
<td>
<button class="btn btn-info sm" @click="open_record_content(record.record.id)">查看录像</button>
</td>
</tr>
</tbody>
</table>
<nav aria-label="..." style="float: right;">
<ul class="pagination">
<li class="page-item">
<a class="page-link" href="#" @click="click_page(-2)">上一页</a>
</li>
<li :class="'page-item ' + page.is_active" v-for="page in pages" :key="page.number" @click="click_page(page.number)">
<a class="page-link" href="#">{{page.number}}</a>
</li>
<li class="page-item">
<a class="page-link" href="#" @click="click_page(-1)">下一页</a>
</li>
</ul>
</nav>
</ContentField>
</template>
<script>
import ContentField from '../../components/ContentField.vue';
import { useStore } from 'vuex';
import $ from 'jquery';
import { ref } from 'vue';
export default {
components: {
ContentField
},
setup() {
const store = useStore();
let currentPage = 1;
let total_records = 0; //
let records = ref([]); //
let pages = ref([]); //
const updatePages = () => {
let max_pages = parseInt(Math.ceil(total_records / 8));
let new_pages = [];
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
if (i >= 1 && i <= max_pages) {
new_pages.push({
number: i,
is_active: i === currentPage ? "active" : "",
})
}
}
pages.value = new_pages;
}
const click_page = page => {
if (page === -2) page = currentPage - 1;
else if (page === -1) page = currentPage + 1;
let max_pages = parseInt(Math.ceil(total_records / 8));
if (page >= 1 && page <= max_pages) {
pull_page(page);
}
}
const pull_page = page => {
currentPage = page;
$.ajax({
url: "https://kob.bnblogs.cc/api/record/list/",
type: "GET",
data: {
page
},
headers:{
"Authorization": "Bearer " + store.state.user.token, //
},
success(resp){
records.value = resp.records;
total_records = resp.records_count;
updatePages();
}
})
}
pull_page(currentPage);
const stringTo2D = map => {
let g = [];
for (let i = 0, k = 0; i < 13; i++) {
let line = [];
for (let j = 0; j < 14; j++,k++) {
if(map[k] === '0') {
line.push(0);
}else {
line.push(1);
}
}
g.push(line);
}
return g;
}
// recordId
const open_record_content = (recordId) => {
for (const record of records.value) {
if (record.record.id === recordId) {
store.commit("updateIsRecord",true); //
//
store.commit("updateGame",{
map: stringTo2D(record.record.map),
a_id: record.record.aid,
a_sx: record.record.asx,
a_sy: record.record.asy,
b_id: record.record.bid,
b_sx: record.record.bsx,
b_sy: record.record.bsy,
});
//
store.commit("updateSteps",{
a_steps: record.record.asteps,
b_steps: record.record.bsteps,
})
//
store.commit("updateRecordLoser",record.record.loser);
break;
}
}
}
return {
open_record_content,
click_page,
records,
pages,
}
}
}
</script>
<style scoped>
.user_photo{
width: 4vh;
border-radius: 50%;
}
.username {
font-weight: bold;
font-size: 1vw;
}
.userA {
color: blue;
}
.userB {
color: red;
}
</style>

@ -0,0 +1,279 @@
<template>
<ContentField>
<div class="container">
<div class="row">
<div class="col-3">
<div class="card" style="margin-top: 30px">
<div class="card-body">
<img :src="$store.state.user.photo" alt="">
</div>
</div>
</div>
<div class="col-9">
<div class="card" style="margin-top: 30px">
<div class="card-header" >
<span style="font-size: 20px; font-weight: bold ">我的bots</span>
<button type="button" class="btn btn-primary float-end" btn-sm data-bs-toggle="modal" data-bs-target="#add_bot_btn">创建bot</button>
<!-- Modal -->
<div class="modal fade" id="add_bot_btn" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">创建新的bot</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="add_bot_title" class="form-label">bot名称</label>
<input type="text" v-model="new_bot.title" class="form-control" id="add_bot_title" placeholder="请输入新bot的名称">
</div>
<div class="mb-3">
<label for="add_bot_description" class="form-label">bot简介</label>
<textarea class="form-control" v-model="new_bot.description" id="add_bot_description" rows= "3" placeholder="请输入新bot的简介"></textarea>
</div>
<div class="mb-3">
<label for="add_bot_code" class="form-label">bot的代码</label>
<VAceEditor
v-model:value="new_bot.content"
@init="editorInit"
lang="c_cpp"
theme="textmate"
:options="{fontSize: 16}"
style="height: 300px" />
</div>
</form>
</div>
<div class="modal-footer">
<div class="error_msg">{{ new_bot.error_msg }}</div>
<button type="button" class="btn btn-success" @click="add_bot" btn-sm>创建</button>
<button type="button" class="btn btn-danger" btn-sm @click="cancel_add">取消</button>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<table class="table table-striped table-hover">
<!--表头-->
<thead>
<tr>
<th>名称</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<!--内容-->
<tbody>
<tr v-for="bot in bots" :key="bot.id">
<td>{{bot.title}}</td>
<td>{{bot.createTime}}</td>
<td>
<button type="button" class="btn btn-success" btn-sm style="margin-right: 10px" data-bs-toggle="modal" :data-bs-target="'#update-bot-btn-'+bot.id" >修改</button>
<button type="button" class="btn btn-danger" btn-sm @click="remove_bot(bot)">删除</button>
<div class="modal fade" :id="'update-bot-btn-'+bot.id" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">修改bot信息</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="add_bot_title" class="form-label">bot名称</label>
<input type="text" v-model="bot.title" class="form-control" id="add_bot_title" >
</div>
<div class="mb-3">
<label for="add_bot_description" class="form-label">bot简介</label>
<textarea class="form-control" v-model="bot.description" id="add_bot_description" rows= "3" ></textarea>
</div>
<div class="mb-3">
<label for="add_bot_code" class="form-label">bot的代码</label>
<VAceEditor
v-model:value="bot.content"
@init="editorInit"
lang="c_cpp"
theme="textmate"
:options="{fontSize: 16}"
style="height: 300px" />
</div>
</form>
</div>
<div class="modal-footer">
<div class="error_msg">{{ bot.error_msg }}</div>
<button type="button" class="btn btn-success" @click="update_bot(bot)" btn-sm>保存修改</button>
<button type="button" class="btn btn-danger" btn-sm @click="cancel_update(bot)">取消</button>
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</ContentField>
</template>
<script>
import { ref,reactive } from "vue"
import $ from 'jquery'
import { useStore } from "vuex"
import {Modal} from "bootstrap/dist/js/bootstrap"
import ContentField from "@/components/ContentField.vue"
import { VAceEditor } from 'vue3-ace-editor';
import ace from 'ace-builds';
export default {
components:{
VAceEditor,
ContentField,
},
setup() {
ace.config.set(
"basePath",
"https://cdn.jsdelivr.net/npm/ace-builds@" + require('ace-builds').version + "/src-noconflict/");
const store = useStore();
let bots = ref([]) // bot
const new_bot = reactive({
title: "",
description: "",
content: "",
error_msg: "",
})
const cancel_update = (bot)=> {
Modal.getInstance("#update-bot-btn-"+bot.id).hide();
refresh_bots();
}
const cancel_add = ()=> {
Modal.getInstance("#add_bot_btn").hide();
refresh_bots();
}
const refresh_bots = () => {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/bot/getlist/",
type: "GET",
headers:{
"Authorization": "Bearer " + store.state.user.token, //
},
success(resp){
// bot
bots.value = resp;
}
})
}
refresh_bots();
// bot
const add_bot = () => {
new_bot.error_msg = "";
$.ajax({
url: "https://kob.bnblogs.cc/api/user/bot/add/",
type: "POST",
data: {
title: new_bot.title,
description: new_bot.description,
content: new_bot.content,
},
headers:{
"Authorization": "Bearer " + store.state.user.token,
},
success(resp) {
if (resp.error_msg === "success") { // bot
new_bot.title = "",
new_bot.description="",
new_bot.content="",
Modal.getInstance("#add_bot_btn").hide();
refresh_bots();
}
else {
new_bot.error_msg = resp.error_msg; //
}
}
})
}
// bot
const update_bot = (bot) => {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/bot/update/",
type: "POST",
data: {
bot_id: bot.id,
title: bot.title,
description: bot.description,
content: bot.content,
},
headers:{
"Authorization": "Bearer " + store.state.user.token,
},
success(resp) {
if (resp.error_msg === "success") { // bot
Modal.getInstance("#update-bot-btn-"+bot.id).hide();
refresh_bots();
}
else {
new_bot.error_msg = resp.error_msg; //
}
}
})
}
// bot
const remove_bot = (bot) => {
$.ajax({
url: "https://kob.bnblogs.cc/api/user/bot/remove/",
type: "POST",
data: {
bot_id: bot.id
},
headers:{
"Authorization": "Bearer " + store.state.user.token,
},
success(resp) {
console.log(resp)
if (resp.error_msg === "success") { // bot
refresh_bots();
}
}
})
}
return {
bots,
new_bot,
add_bot,
remove_bot,
update_bot,
cancel_update,
cancel_add,
}
}
}
</script>
<style scoped>
img{
width: 100%
}
.error_msg{
font-size: 20px;
font-weight: bold;
color: red;
}
</style>

@ -1,4 +1,9 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
transpileDependencies: true,
configureWebpack: {
optimization: {
splitChunks: false
}
}
})
Loading…
Cancel
Save