├── .gitignore
├── README.md
├── babel.config.js
├── index.html
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── server
├── index.js
├── package-lock.json
├── package.json
└── yarn.lock
├── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ ├── Box.vue
│ └── Game.vue
├── game
│ ├── Box.js
│ ├── Game.js
│ ├── Player.js
│ ├── Rival.js
│ ├── StateManagement.js
│ ├── config.js
│ ├── eliminate.js
│ ├── hit.js
│ ├── index.js
│ ├── map.js
│ ├── matrix.js
│ ├── renderer.js
│ └── ticker.js
├── main.js
└── utils
│ └── socket.js
├── tests
├── eliminate.spec.js
├── hit.spec.js
├── map.spec.js
└── matrix.spec.js
├── vite.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tetris-vue3
2 |
3 | 使用 vue3 实现俄罗斯方块
4 |
5 | [单机版本实现](https://github.com/cuixiaorui/tetris-vue3/tree/stand-alone)
6 | [联机版本实现](https://github.com/cuixiaorui/tetris-vue3/tree/online)
7 |
8 | ## 实现原理
9 |
10 | 采用了 [Functional Core, Imperative Shell](https://marsbased.com/blog/2020/01/20/functional-core-imperative-shell/#:~:text=The%20pattern.%20This%20pattern%20is%20sometimes%20called%20functional,commands.%20We%20keep%20that%20code%20small%20and%20trivial.) 模式来实现
11 |
12 | 提高了可测试性
13 |
14 | 业务核心逻辑和视图逻辑拆分
15 |
16 | 可以移植到任意 UI 库
17 |
18 |
19 |
20 |
21 | ## todo
22 |
23 | - [ ] 游戏重来
24 |
25 |
26 | # 收获
27 |
28 | - 应用程序从 0 到 1 的全过程
29 |
30 | - 用户故事来描述你的程序需求点
31 |
32 | - tasking 的方式来管理你的开发进度
33 |
34 | - vue3 最新的 setup script 语法糖的应用
35 |
36 | - 使用单元测试提高开发效率
37 |
38 | - 设计模式的应用
39 |
40 | - 策略模式
41 |
42 | - 工厂模式
43 |
44 | - 重构技巧 (写出好代码 )
45 |
46 |
47 | ## Tasking
48 | ### 单机
49 |
50 | - 用户进入游戏的时候可以看到游戏开始页面
51 |
52 | 
53 |
54 |
55 |
56 | - 用户点击 startGame 可以开始游戏
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 | 通过 websocket 来同步玩家的动作,来实现双人对战模式
108 |
109 | ### 同步的动作
110 |
111 | - gameOver (游戏结束)
112 |
113 | - to other
114 | - gameWon
115 |
116 | - eliminateLine (消除行)
117 |
118 | - to self
119 |
120 | - syncAddLine (同步 dival 视图)
121 |
122 | - to other
123 | - addLine (让其他玩家加行)
124 |
125 | - moveBoxToDown (向下移动 box)
126 |
127 | - to other
128 | - moveBoxToDown
129 |
130 | - moveBoxToLeft (向左移动 box)
131 |
132 | - to other
133 | - moveBoxToLeft
134 |
135 | - moveBoxToRight (向右移动 box)
136 |
137 | - to other
138 | - moveBoxToRight
139 |
140 | - rotateBox (旋转 box)
141 |
142 | - to other
143 | - rotateBox
144 |
145 | - createBox (创建 box)
146 | - to other
147 | - createBox
148 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // babel.config.js
2 | module.exports = {
3 | presets: [
4 | ["@babel/preset-env", { targets: { node: "current" } }],
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.0",
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "vite build",
6 | "serve": "vite preview",
7 | "test": "jest"
8 | },
9 | "dependencies": {
10 | "@babel/polyfill": "^7.12.1",
11 | "mitt": "^3.0.0",
12 | "socket.io-client": "^4.1.2",
13 | "vue": "^3.0.5"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.14.6",
17 | "@babel/preset-env": "^7.14.7",
18 | "@types/jest": "^26.0.23",
19 | "@vitejs/plugin-vue": "^1.2.3",
20 | "@vue/compiler-sfc": "^3.0.5",
21 | "babel-jest": "^27.0.6",
22 | "jest": "^27.0.6",
23 | "vite": "^2.3.8"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/tetris-vue3/f65735fbcc1223686a65af1e64a91e045f09f45c/public/favicon.ico
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require("koa");
2 | const app = new Koa();
3 | const server = require("http").createServer(app.callback());
4 |
5 | const io = require("socket.io")(server, {
6 | cors: {
7 | origin: "http://localhost:3000",
8 | methods: ["GET", "POST"],
9 | credentials: true,
10 | },
11 | });
12 |
13 | io.on("connection", (socket) => {
14 | console.log("a user connected");
15 | socket.on("gameOver", (state) => {
16 | socket.broadcast.emit("gameWon", state);
17 | });
18 |
19 | // 1. 接受 user 发过来的数据
20 | // 2. 广播给其他的 user
21 | socket.on("eliminateLine", (num) => {
22 | // 玩家消行了
23 | // 1. 让其他的玩家加行
24 | // 2. 同步自己的 Dival 视图
25 | socket.emit("syncAddLine", num);
26 | socket.broadcast.emit("addLine", num);
27 | });
28 | socket.on("moveBoxToDown", (info) => {
29 | socket.broadcast.emit("moveBoxToDown", info);
30 | });
31 | socket.on("moveBoxToLeft", (info) => {
32 | socket.broadcast.emit("moveBoxToLeft", info);
33 | });
34 | socket.on("moveBoxToRight", (info) => {
35 | socket.broadcast.emit("moveBoxToRight", info);
36 | });
37 | socket.on("rotateBox", (info) => {
38 | socket.broadcast.emit("rotateBox", info);
39 | });
40 | socket.on("createBox", (info) => {
41 | socket.broadcast.emit("createBox", info);
42 | });
43 | });
44 |
45 | server.listen(3001, () => {
46 | console.log("listening on *:3001");
47 | });
48 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@koa/cors": {
8 | "version": "3.1.0",
9 | "resolved": "https://registry.npm.taobao.org/@koa/cors/download/@koa/cors-3.1.0.tgz",
10 | "integrity": "sha1-YYuwc0OM/b0+vQ5kinbjO4Tzo7I=",
11 | "requires": {
12 | "vary": "^1.1.2"
13 | }
14 | },
15 | "@types/component-emitter": {
16 | "version": "1.2.10",
17 | "resolved": "https://registry.nlark.com/@types/component-emitter/download/@types/component-emitter-1.2.10.tgz",
18 | "integrity": "sha1-71sVibnxZURkLkc9tepWORB+8+o="
19 | },
20 | "@types/cookie": {
21 | "version": "0.4.1",
22 | "resolved": "https://registry.nlark.com/@types/cookie/download/@types/cookie-0.4.1.tgz",
23 | "integrity": "sha1-v9AsHyIkVnZ2wVRRmfh8OoYdh40="
24 | },
25 | "@types/cors": {
26 | "version": "2.8.12",
27 | "resolved": "https://registry.nlark.com/@types/cors/download/@types/cors-2.8.12.tgz?cache=0&sync_timestamp=1625816589458&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fcors%2Fdownload%2F%40types%2Fcors-2.8.12.tgz",
28 | "integrity": "sha1-ayxRCnrXA56Y57jT1lmPQ1nlwIA="
29 | },
30 | "@types/node": {
31 | "version": "16.3.1",
32 | "resolved": "https://registry.nlark.com/@types/node/download/@types/node-16.3.1.tgz?cache=0&sync_timestamp=1625868289017&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-16.3.1.tgz",
33 | "integrity": "sha1-JGkforDD7IwNNL/P1JXtrFWT67Q="
34 | },
35 | "accepts": {
36 | "version": "1.3.7",
37 | "resolved": "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz",
38 | "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=",
39 | "requires": {
40 | "mime-types": "~2.1.24",
41 | "negotiator": "0.6.2"
42 | }
43 | },
44 | "any-promise": {
45 | "version": "1.3.0",
46 | "resolved": "https://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz",
47 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
48 | },
49 | "base64-arraybuffer": {
50 | "version": "0.1.4",
51 | "resolved": "https://registry.npm.taobao.org/base64-arraybuffer/download/base64-arraybuffer-0.1.4.tgz?cache=0&sync_timestamp=1586263725228&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbase64-arraybuffer%2Fdownload%2Fbase64-arraybuffer-0.1.4.tgz",
52 | "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
53 | },
54 | "base64id": {
55 | "version": "2.0.0",
56 | "resolved": "https://registry.npm.taobao.org/base64id/download/base64id-2.0.0.tgz",
57 | "integrity": "sha1-J3Csa8R9MSr5eov5pjQ0LgzSXLY="
58 | },
59 | "cache-content-type": {
60 | "version": "1.0.1",
61 | "resolved": "https://registry.npm.taobao.org/cache-content-type/download/cache-content-type-1.0.1.tgz",
62 | "integrity": "sha1-A1zeKwjuISn0qDFeqPAKANuhRTw=",
63 | "requires": {
64 | "mime-types": "^2.1.18",
65 | "ylru": "^1.2.0"
66 | }
67 | },
68 | "co": {
69 | "version": "4.6.0",
70 | "resolved": "https://registry.npm.taobao.org/co/download/co-4.6.0.tgz",
71 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
72 | },
73 | "component-emitter": {
74 | "version": "1.3.0",
75 | "resolved": "https://registry.npm.taobao.org/component-emitter/download/component-emitter-1.3.0.tgz",
76 | "integrity": "sha1-FuQHD7qK4ptnnyIVhT7hgasuq8A="
77 | },
78 | "content-disposition": {
79 | "version": "0.5.3",
80 | "resolved": "https://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz",
81 | "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=",
82 | "requires": {
83 | "safe-buffer": "5.1.2"
84 | }
85 | },
86 | "content-type": {
87 | "version": "1.0.4",
88 | "resolved": "https://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz",
89 | "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js="
90 | },
91 | "cookies": {
92 | "version": "0.8.0",
93 | "resolved": "https://registry.npm.taobao.org/cookies/download/cookies-0.8.0.tgz?cache=0&sync_timestamp=1570851324736&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcookies%2Fdownload%2Fcookies-0.8.0.tgz",
94 | "integrity": "sha1-EpPOSzkXQKhAbjyYcOgoxLVPP5A=",
95 | "requires": {
96 | "depd": "~2.0.0",
97 | "keygrip": "~1.1.0"
98 | },
99 | "dependencies": {
100 | "depd": {
101 | "version": "2.0.0",
102 | "resolved": "https://registry.npm.taobao.org/depd/download/depd-2.0.0.tgz",
103 | "integrity": "sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8="
104 | }
105 | }
106 | },
107 | "cors": {
108 | "version": "2.8.5",
109 | "resolved": "https://registry.npm.taobao.org/cors/download/cors-2.8.5.tgz",
110 | "integrity": "sha1-6sEdpRWS3Ya58G9uesKTs9+HXSk=",
111 | "requires": {
112 | "object-assign": "^4",
113 | "vary": "^1"
114 | }
115 | },
116 | "deep-equal": {
117 | "version": "1.0.1",
118 | "resolved": "https://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz?cache=0&sync_timestamp=1606860101281&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeep-equal%2Fdownload%2Fdeep-equal-1.0.1.tgz",
119 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
120 | },
121 | "delegates": {
122 | "version": "1.0.0",
123 | "resolved": "https://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz",
124 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
125 | },
126 | "depd": {
127 | "version": "1.1.2",
128 | "resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
129 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
130 | },
131 | "destroy": {
132 | "version": "1.0.4",
133 | "resolved": "https://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz",
134 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
135 | },
136 | "ee-first": {
137 | "version": "1.1.1",
138 | "resolved": "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
139 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
140 | },
141 | "encodeurl": {
142 | "version": "1.0.2",
143 | "resolved": "https://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.2.tgz",
144 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
145 | },
146 | "engine.io": {
147 | "version": "5.1.1",
148 | "resolved": "https://registry.nlark.com/engine.io/download/engine.io-5.1.1.tgz?cache=0&sync_timestamp=1621204387437&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fengine.io%2Fdownload%2Fengine.io-5.1.1.tgz",
149 | "integrity": "sha1-ofl+Ud3xDL1NuLX/SxZarTdgzdM=",
150 | "requires": {
151 | "accepts": "~1.3.4",
152 | "base64id": "2.0.0",
153 | "cookie": "~0.4.1",
154 | "cors": "~2.8.5",
155 | "debug": "~4.3.1",
156 | "engine.io-parser": "~4.0.0",
157 | "ws": "~7.4.2"
158 | },
159 | "dependencies": {
160 | "cookie": {
161 | "version": "0.4.1",
162 | "resolved": "https://registry.npm.taobao.org/cookie/download/cookie-0.4.1.tgz",
163 | "integrity": "sha1-r9cT/ibr0hupXOth+agRblClN9E="
164 | },
165 | "debug": {
166 | "version": "4.3.2",
167 | "resolved": "https://registry.nlark.com/debug/download/debug-4.3.2.tgz",
168 | "integrity": "sha1-8KScGKyHeeMdSgxgKd+3aHPHQos=",
169 | "requires": {
170 | "ms": "2.1.2"
171 | }
172 | },
173 | "ms": {
174 | "version": "2.1.2",
175 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
176 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
177 | }
178 | }
179 | },
180 | "engine.io-parser": {
181 | "version": "4.0.2",
182 | "resolved": "https://registry.npm.taobao.org/engine.io-parser/download/engine.io-parser-4.0.2.tgz?cache=0&sync_timestamp=1607330820767&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fengine.io-parser%2Fdownload%2Fengine.io-parser-4.0.2.tgz",
183 | "integrity": "sha1-5B0LP7Zve/SjZx0gOKFUAk7bUB4=",
184 | "requires": {
185 | "base64-arraybuffer": "0.1.4"
186 | }
187 | },
188 | "escape-html": {
189 | "version": "1.0.3",
190 | "resolved": "https://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz",
191 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
192 | },
193 | "fresh": {
194 | "version": "0.5.2",
195 | "resolved": "https://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz",
196 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
197 | },
198 | "http-assert": {
199 | "version": "1.4.1",
200 | "resolved": "https://registry.npm.taobao.org/http-assert/download/http-assert-1.4.1.tgz",
201 | "integrity": "sha1-xfcl1neqfoc+9zYZm4lobM6zeHg=",
202 | "requires": {
203 | "deep-equal": "~1.0.1",
204 | "http-errors": "~1.7.2"
205 | }
206 | },
207 | "http-errors": {
208 | "version": "1.7.2",
209 | "resolved": "https://registry.npm.taobao.org/http-errors/download/http-errors-1.7.2.tgz",
210 | "integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=",
211 | "requires": {
212 | "depd": "~1.1.2",
213 | "inherits": "2.0.3",
214 | "setprototypeof": "1.1.1",
215 | "statuses": ">= 1.5.0 < 2",
216 | "toidentifier": "1.0.0"
217 | }
218 | },
219 | "inherits": {
220 | "version": "2.0.3",
221 | "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz",
222 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
223 | },
224 | "is-generator-function": {
225 | "version": "1.0.9",
226 | "resolved": "https://registry.nlark.com/is-generator-function/download/is-generator-function-1.0.9.tgz?cache=0&sync_timestamp=1620280979070&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fis-generator-function%2Fdownload%2Fis-generator-function-1.0.9.tgz",
227 | "integrity": "sha1-5fgsIyNnPn/K09EoWMg8QDn2OZw="
228 | },
229 | "keygrip": {
230 | "version": "1.1.0",
231 | "resolved": "https://registry.npm.taobao.org/keygrip/download/keygrip-1.1.0.tgz",
232 | "integrity": "sha1-hxsWgdXhWcYqRFsMdLYV4JF+ciY=",
233 | "requires": {
234 | "tsscmp": "1.0.6"
235 | }
236 | },
237 | "koa": {
238 | "version": "2.13.1",
239 | "resolved": "https://registry.nlark.com/koa/download/koa-2.13.1.tgz",
240 | "integrity": "sha1-YnUXKHWye8/h1FQ1altrn1qbEFE=",
241 | "requires": {
242 | "accepts": "^1.3.5",
243 | "cache-content-type": "^1.0.0",
244 | "content-disposition": "~0.5.2",
245 | "content-type": "^1.0.4",
246 | "cookies": "~0.8.0",
247 | "debug": "~3.1.0",
248 | "delegates": "^1.0.0",
249 | "depd": "^2.0.0",
250 | "destroy": "^1.0.4",
251 | "encodeurl": "^1.0.2",
252 | "escape-html": "^1.0.3",
253 | "fresh": "~0.5.2",
254 | "http-assert": "^1.3.0",
255 | "http-errors": "^1.6.3",
256 | "is-generator-function": "^1.0.7",
257 | "koa-compose": "^4.1.0",
258 | "koa-convert": "^1.2.0",
259 | "on-finished": "^2.3.0",
260 | "only": "~0.0.2",
261 | "parseurl": "^1.3.2",
262 | "statuses": "^1.5.0",
263 | "type-is": "^1.6.16",
264 | "vary": "^1.1.2"
265 | },
266 | "dependencies": {
267 | "debug": {
268 | "version": "3.1.0",
269 | "resolved": "https://registry.nlark.com/debug/download/debug-3.1.0.tgz",
270 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
271 | "requires": {
272 | "ms": "2.0.0"
273 | }
274 | },
275 | "depd": {
276 | "version": "2.0.0",
277 | "resolved": "https://registry.npm.taobao.org/depd/download/depd-2.0.0.tgz",
278 | "integrity": "sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8="
279 | }
280 | }
281 | },
282 | "koa-compose": {
283 | "version": "4.1.0",
284 | "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-4.1.0.tgz",
285 | "integrity": "sha1-UHMGuTcZAdtBEhyBLpI9DWfT6Hc="
286 | },
287 | "koa-convert": {
288 | "version": "1.2.0",
289 | "resolved": "https://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz",
290 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=",
291 | "requires": {
292 | "co": "^4.6.0",
293 | "koa-compose": "^3.0.0"
294 | },
295 | "dependencies": {
296 | "koa-compose": {
297 | "version": "3.2.1",
298 | "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz",
299 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=",
300 | "requires": {
301 | "any-promise": "^1.1.0"
302 | }
303 | }
304 | }
305 | },
306 | "koa-cors": {
307 | "version": "0.0.16",
308 | "resolved": "https://registry.npm.taobao.org/koa-cors/download/koa-cors-0.0.16.tgz",
309 | "integrity": "sha1-mBB5k6eQnjTAQphsXsYVbXfzQy4="
310 | },
311 | "media-typer": {
312 | "version": "0.3.0",
313 | "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz",
314 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
315 | },
316 | "mime-db": {
317 | "version": "1.48.0",
318 | "resolved": "https://registry.nlark.com/mime-db/download/mime-db-1.48.0.tgz?cache=0&sync_timestamp=1622433567590&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmime-db%2Fdownload%2Fmime-db-1.48.0.tgz",
319 | "integrity": "sha1-41sxBF3X6to6qtU37YijOvvvLR0="
320 | },
321 | "mime-types": {
322 | "version": "2.1.31",
323 | "resolved": "https://registry.nlark.com/mime-types/download/mime-types-2.1.31.tgz?cache=0&sync_timestamp=1622569304088&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmime-types%2Fdownload%2Fmime-types-2.1.31.tgz",
324 | "integrity": "sha1-oA12t0MXxh+cLbIhi46fjpxcnms=",
325 | "requires": {
326 | "mime-db": "1.48.0"
327 | }
328 | },
329 | "ms": {
330 | "version": "2.0.0",
331 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz",
332 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
333 | },
334 | "negotiator": {
335 | "version": "0.6.2",
336 | "resolved": "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz",
337 | "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs="
338 | },
339 | "object-assign": {
340 | "version": "4.1.1",
341 | "resolved": "https://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz",
342 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
343 | },
344 | "on-finished": {
345 | "version": "2.3.0",
346 | "resolved": "https://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz",
347 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
348 | "requires": {
349 | "ee-first": "1.1.1"
350 | }
351 | },
352 | "only": {
353 | "version": "0.0.2",
354 | "resolved": "https://registry.npm.taobao.org/only/download/only-0.0.2.tgz",
355 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q="
356 | },
357 | "parseurl": {
358 | "version": "1.3.3",
359 | "resolved": "https://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz",
360 | "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ="
361 | },
362 | "safe-buffer": {
363 | "version": "5.1.2",
364 | "resolved": "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz",
365 | "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
366 | },
367 | "setprototypeof": {
368 | "version": "1.1.1",
369 | "resolved": "https://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.1.tgz",
370 | "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM="
371 | },
372 | "socket.io": {
373 | "version": "4.1.2",
374 | "resolved": "https://registry.nlark.com/socket.io/download/socket.io-4.1.2.tgz",
375 | "integrity": "sha1-+Q+QAqjVUO/iqh0yDe67mkW4MjM=",
376 | "requires": {
377 | "@types/cookie": "^0.4.0",
378 | "@types/cors": "^2.8.8",
379 | "@types/node": ">=10.0.0",
380 | "accepts": "~1.3.4",
381 | "base64id": "~2.0.0",
382 | "debug": "~4.3.1",
383 | "engine.io": "~5.1.0",
384 | "socket.io-adapter": "~2.3.0",
385 | "socket.io-parser": "~4.0.3"
386 | },
387 | "dependencies": {
388 | "debug": {
389 | "version": "4.3.2",
390 | "resolved": "https://registry.nlark.com/debug/download/debug-4.3.2.tgz",
391 | "integrity": "sha1-8KScGKyHeeMdSgxgKd+3aHPHQos=",
392 | "requires": {
393 | "ms": "2.1.2"
394 | }
395 | },
396 | "ms": {
397 | "version": "2.1.2",
398 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
399 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
400 | }
401 | }
402 | },
403 | "socket.io-adapter": {
404 | "version": "2.3.1",
405 | "resolved": "https://registry.nlark.com/socket.io-adapter/download/socket.io-adapter-2.3.1.tgz",
406 | "integrity": "sha1-pEJyDLCaSCPPuBKH3aH5tS1MzbI="
407 | },
408 | "socket.io-parser": {
409 | "version": "4.0.4",
410 | "resolved": "https://registry.nlark.com/socket.io-parser/download/socket.io-parser-4.0.4.tgz",
411 | "integrity": "sha1-nqIbDWFQjRgZbvBKLGuatjD0wrA=",
412 | "requires": {
413 | "@types/component-emitter": "^1.2.10",
414 | "component-emitter": "~1.3.0",
415 | "debug": "~4.3.1"
416 | },
417 | "dependencies": {
418 | "debug": {
419 | "version": "4.3.2",
420 | "resolved": "https://registry.nlark.com/debug/download/debug-4.3.2.tgz",
421 | "integrity": "sha1-8KScGKyHeeMdSgxgKd+3aHPHQos=",
422 | "requires": {
423 | "ms": "2.1.2"
424 | }
425 | },
426 | "ms": {
427 | "version": "2.1.2",
428 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
429 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
430 | }
431 | }
432 | },
433 | "statuses": {
434 | "version": "1.5.0",
435 | "resolved": "https://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz?cache=0&sync_timestamp=1609654438540&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fstatuses%2Fdownload%2Fstatuses-1.5.0.tgz",
436 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
437 | },
438 | "toidentifier": {
439 | "version": "1.0.0",
440 | "resolved": "https://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz",
441 | "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM="
442 | },
443 | "tsscmp": {
444 | "version": "1.0.6",
445 | "resolved": "https://registry.npm.taobao.org/tsscmp/download/tsscmp-1.0.6.tgz",
446 | "integrity": "sha1-hbmVg6w1iexL/vgltQAKqRHWBes="
447 | },
448 | "type-is": {
449 | "version": "1.6.18",
450 | "resolved": "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz",
451 | "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=",
452 | "requires": {
453 | "media-typer": "0.3.0",
454 | "mime-types": "~2.1.24"
455 | }
456 | },
457 | "vary": {
458 | "version": "1.1.2",
459 | "resolved": "https://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz",
460 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
461 | },
462 | "ws": {
463 | "version": "7.4.6",
464 | "resolved": "https://registry.nlark.com/ws/download/ws-7.4.6.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fws%2Fdownload%2Fws-7.4.6.tgz",
465 | "integrity": "sha1-VlTKjs3u5HwzqaS/bSjivimAN3w="
466 | },
467 | "ylru": {
468 | "version": "1.2.1",
469 | "resolved": "https://registry.npm.taobao.org/ylru/download/ylru-1.2.1.tgz",
470 | "integrity": "sha1-9Xa2M0FUeYnB3nuiiHYJI7J/6E8="
471 | }
472 | }
473 | }
474 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "koa": "^2.13.1",
14 | "socket.io": "^4.1.2"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/component-emitter@^1.2.10":
6 | version "1.2.10"
7 | resolved "https://registry.nlark.com/@types/component-emitter/download/@types/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
8 | integrity sha1-71sVibnxZURkLkc9tepWORB+8+o=
9 |
10 | "@types/cookie@^0.4.0":
11 | version "0.4.1"
12 | resolved "https://registry.nlark.com/@types/cookie/download/@types/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
13 | integrity sha1-v9AsHyIkVnZ2wVRRmfh8OoYdh40=
14 |
15 | "@types/cors@^2.8.8":
16 | version "2.8.12"
17 | resolved "https://registry.nlark.com/@types/cors/download/@types/cors-2.8.12.tgz?cache=0&sync_timestamp=1625816589458&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fcors%2Fdownload%2F%40types%2Fcors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
18 | integrity sha1-ayxRCnrXA56Y57jT1lmPQ1nlwIA=
19 |
20 | "@types/node@>=10.0.0":
21 | version "16.3.1"
22 | resolved "https://registry.nlark.com/@types/node/download/@types/node-16.3.1.tgz?cache=0&sync_timestamp=1625868289017&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-16.3.1.tgz#24691fa2b0c3ec8c0d34bfcfd495edac5593ebb4"
23 | integrity sha1-JGkforDD7IwNNL/P1JXtrFWT67Q=
24 |
25 | accepts@^1.3.5, accepts@~1.3.4:
26 | version "1.3.7"
27 | resolved "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
28 | integrity sha1-UxvHJlF6OytB+FACHGzBXqq1B80=
29 | dependencies:
30 | mime-types "~2.1.24"
31 | negotiator "0.6.2"
32 |
33 | any-promise@^1.1.0:
34 | version "1.3.0"
35 | resolved "https://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
36 | integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
37 |
38 | base64-arraybuffer@0.1.4:
39 | version "0.1.4"
40 | resolved "https://registry.npm.taobao.org/base64-arraybuffer/download/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
41 | integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
42 |
43 | base64id@2.0.0, base64id@~2.0.0:
44 | version "2.0.0"
45 | resolved "https://registry.npm.taobao.org/base64id/download/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
46 | integrity sha1-J3Csa8R9MSr5eov5pjQ0LgzSXLY=
47 |
48 | cache-content-type@^1.0.0:
49 | version "1.0.1"
50 | resolved "https://registry.npm.taobao.org/cache-content-type/download/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
51 | integrity sha1-A1zeKwjuISn0qDFeqPAKANuhRTw=
52 | dependencies:
53 | mime-types "^2.1.18"
54 | ylru "^1.2.0"
55 |
56 | co@^4.6.0:
57 | version "4.6.0"
58 | resolved "https://registry.npm.taobao.org/co/download/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
59 | integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
60 |
61 | component-emitter@~1.3.0:
62 | version "1.3.0"
63 | resolved "https://registry.nlark.com/component-emitter/download/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
64 | integrity sha1-FuQHD7qK4ptnnyIVhT7hgasuq8A=
65 |
66 | content-disposition@~0.5.2:
67 | version "0.5.3"
68 | resolved "https://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
69 | integrity sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=
70 | dependencies:
71 | safe-buffer "5.1.2"
72 |
73 | content-type@^1.0.4:
74 | version "1.0.4"
75 | resolved "https://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
76 | integrity sha1-4TjMdeBAxyexlm/l5fjJruJW/js=
77 |
78 | cookie@~0.4.1:
79 | version "0.4.1"
80 | resolved "https://registry.npm.taobao.org/cookie/download/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
81 | integrity sha1-r9cT/ibr0hupXOth+agRblClN9E=
82 |
83 | cookies@~0.8.0:
84 | version "0.8.0"
85 | resolved "https://registry.npm.taobao.org/cookies/download/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
86 | integrity sha1-EpPOSzkXQKhAbjyYcOgoxLVPP5A=
87 | dependencies:
88 | depd "~2.0.0"
89 | keygrip "~1.1.0"
90 |
91 | cors@~2.8.5:
92 | version "2.8.5"
93 | resolved "https://registry.npm.taobao.org/cors/download/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
94 | integrity sha1-6sEdpRWS3Ya58G9uesKTs9+HXSk=
95 | dependencies:
96 | object-assign "^4"
97 | vary "^1"
98 |
99 | debug@~3.1.0:
100 | version "3.1.0"
101 | resolved "https://registry.nlark.com/debug/download/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
102 | integrity sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=
103 | dependencies:
104 | ms "2.0.0"
105 |
106 | debug@~4.3.1:
107 | version "4.3.2"
108 | resolved "https://registry.nlark.com/debug/download/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
109 | integrity sha1-8KScGKyHeeMdSgxgKd+3aHPHQos=
110 | dependencies:
111 | ms "2.1.2"
112 |
113 | deep-equal@~1.0.1:
114 | version "1.0.1"
115 | resolved "https://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz?cache=0&sync_timestamp=1606860101281&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdeep-equal%2Fdownload%2Fdeep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
116 | integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
117 |
118 | delegates@^1.0.0:
119 | version "1.0.0"
120 | resolved "https://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
121 | integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
122 |
123 | depd@^2.0.0, depd@~2.0.0:
124 | version "2.0.0"
125 | resolved "https://registry.npm.taobao.org/depd/download/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
126 | integrity sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8=
127 |
128 | depd@~1.1.2:
129 | version "1.1.2"
130 | resolved "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
131 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
132 |
133 | destroy@^1.0.4:
134 | version "1.0.4"
135 | resolved "https://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
136 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
137 |
138 | ee-first@1.1.1:
139 | version "1.1.1"
140 | resolved "https://registry.nlark.com/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
141 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
142 |
143 | encodeurl@^1.0.2:
144 | version "1.0.2"
145 | resolved "https://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
146 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
147 |
148 | engine.io-parser@~4.0.0:
149 | version "4.0.2"
150 | resolved "https://registry.npm.taobao.org/engine.io-parser/download/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
151 | integrity sha1-5B0LP7Zve/SjZx0gOKFUAk7bUB4=
152 | dependencies:
153 | base64-arraybuffer "0.1.4"
154 |
155 | engine.io@~5.1.0:
156 | version "5.1.1"
157 | resolved "https://registry.nlark.com/engine.io/download/engine.io-5.1.1.tgz?cache=0&sync_timestamp=1621204387437&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fengine.io%2Fdownload%2Fengine.io-5.1.1.tgz#a1f97e51ddf10cbd4db8b5ff4b165aad3760cdd3"
158 | integrity sha1-ofl+Ud3xDL1NuLX/SxZarTdgzdM=
159 | dependencies:
160 | accepts "~1.3.4"
161 | base64id "2.0.0"
162 | cookie "~0.4.1"
163 | cors "~2.8.5"
164 | debug "~4.3.1"
165 | engine.io-parser "~4.0.0"
166 | ws "~7.4.2"
167 |
168 | escape-html@^1.0.3:
169 | version "1.0.3"
170 | resolved "https://registry.nlark.com/escape-html/download/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
171 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
172 |
173 | fresh@~0.5.2:
174 | version "0.5.2"
175 | resolved "https://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
176 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
177 |
178 | http-assert@^1.3.0:
179 | version "1.4.1"
180 | resolved "https://registry.npm.taobao.org/http-assert/download/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878"
181 | integrity sha1-xfcl1neqfoc+9zYZm4lobM6zeHg=
182 | dependencies:
183 | deep-equal "~1.0.1"
184 | http-errors "~1.7.2"
185 |
186 | http-errors@^1.6.3:
187 | version "1.8.0"
188 | resolved "https://registry.npm.taobao.org/http-errors/download/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
189 | integrity sha1-ddG75JfhBE9R5O6ecEpi8o0zZQc=
190 | dependencies:
191 | depd "~1.1.2"
192 | inherits "2.0.4"
193 | setprototypeof "1.2.0"
194 | statuses ">= 1.5.0 < 2"
195 | toidentifier "1.0.0"
196 |
197 | http-errors@~1.7.2:
198 | version "1.7.3"
199 | resolved "https://registry.npm.taobao.org/http-errors/download/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
200 | integrity sha1-bGGeT5xgMIw4UZSYwU+7EKrOuwY=
201 | dependencies:
202 | depd "~1.1.2"
203 | inherits "2.0.4"
204 | setprototypeof "1.1.1"
205 | statuses ">= 1.5.0 < 2"
206 | toidentifier "1.0.0"
207 |
208 | inherits@2.0.4:
209 | version "2.0.4"
210 | resolved "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
211 | integrity sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=
212 |
213 | is-generator-function@^1.0.7:
214 | version "1.0.9"
215 | resolved "https://registry.nlark.com/is-generator-function/download/is-generator-function-1.0.9.tgz?cache=0&sync_timestamp=1620280979070&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fis-generator-function%2Fdownload%2Fis-generator-function-1.0.9.tgz#e5f82c2323673e7fcad3d12858c83c4039f6399c"
216 | integrity sha1-5fgsIyNnPn/K09EoWMg8QDn2OZw=
217 |
218 | keygrip@~1.1.0:
219 | version "1.1.0"
220 | resolved "https://registry.npm.taobao.org/keygrip/download/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
221 | integrity sha1-hxsWgdXhWcYqRFsMdLYV4JF+ciY=
222 | dependencies:
223 | tsscmp "1.0.6"
224 |
225 | koa-compose@^3.0.0:
226 | version "3.2.1"
227 | resolved "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
228 | integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
229 | dependencies:
230 | any-promise "^1.1.0"
231 |
232 | koa-compose@^4.1.0:
233 | version "4.1.0"
234 | resolved "https://registry.npm.taobao.org/koa-compose/download/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
235 | integrity sha1-UHMGuTcZAdtBEhyBLpI9DWfT6Hc=
236 |
237 | koa-convert@^1.2.0:
238 | version "1.2.0"
239 | resolved "https://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz?cache=0&sync_timestamp=1599761789317&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fkoa-convert%2Fdownload%2Fkoa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
240 | integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
241 | dependencies:
242 | co "^4.6.0"
243 | koa-compose "^3.0.0"
244 |
245 | koa@^2.13.1:
246 | version "2.13.1"
247 | resolved "https://registry.npm.taobao.org/koa/download/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051"
248 | integrity sha1-YnUXKHWye8/h1FQ1altrn1qbEFE=
249 | dependencies:
250 | accepts "^1.3.5"
251 | cache-content-type "^1.0.0"
252 | content-disposition "~0.5.2"
253 | content-type "^1.0.4"
254 | cookies "~0.8.0"
255 | debug "~3.1.0"
256 | delegates "^1.0.0"
257 | depd "^2.0.0"
258 | destroy "^1.0.4"
259 | encodeurl "^1.0.2"
260 | escape-html "^1.0.3"
261 | fresh "~0.5.2"
262 | http-assert "^1.3.0"
263 | http-errors "^1.6.3"
264 | is-generator-function "^1.0.7"
265 | koa-compose "^4.1.0"
266 | koa-convert "^1.2.0"
267 | on-finished "^2.3.0"
268 | only "~0.0.2"
269 | parseurl "^1.3.2"
270 | statuses "^1.5.0"
271 | type-is "^1.6.16"
272 | vary "^1.1.2"
273 |
274 | media-typer@0.3.0:
275 | version "0.3.0"
276 | resolved "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
277 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
278 |
279 | mime-db@1.48.0:
280 | version "1.48.0"
281 | resolved "https://registry.nlark.com/mime-db/download/mime-db-1.48.0.tgz?cache=0&sync_timestamp=1622433567590&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmime-db%2Fdownload%2Fmime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
282 | integrity sha1-41sxBF3X6to6qtU37YijOvvvLR0=
283 |
284 | mime-types@^2.1.18, mime-types@~2.1.24:
285 | version "2.1.31"
286 | resolved "https://registry.nlark.com/mime-types/download/mime-types-2.1.31.tgz?cache=0&sync_timestamp=1622569304088&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fmime-types%2Fdownload%2Fmime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
287 | integrity sha1-oA12t0MXxh+cLbIhi46fjpxcnms=
288 | dependencies:
289 | mime-db "1.48.0"
290 |
291 | ms@2.0.0:
292 | version "2.0.0"
293 | resolved "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
294 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
295 |
296 | ms@2.1.2:
297 | version "2.1.2"
298 | resolved "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&sync_timestamp=1607433856030&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
299 | integrity sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=
300 |
301 | negotiator@0.6.2:
302 | version "0.6.2"
303 | resolved "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
304 | integrity sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=
305 |
306 | object-assign@^4:
307 | version "4.1.1"
308 | resolved "https://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
309 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
310 |
311 | on-finished@^2.3.0:
312 | version "2.3.0"
313 | resolved "https://registry.nlark.com/on-finished/download/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
314 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
315 | dependencies:
316 | ee-first "1.1.1"
317 |
318 | only@~0.0.2:
319 | version "0.0.2"
320 | resolved "https://registry.npm.taobao.org/only/download/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
321 | integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
322 |
323 | parseurl@^1.3.2:
324 | version "1.3.3"
325 | resolved "https://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
326 | integrity sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=
327 |
328 | safe-buffer@5.1.2:
329 | version "5.1.2"
330 | resolved "https://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
331 | integrity sha1-mR7GnSluAxN0fVm9/St0XDX4go0=
332 |
333 | setprototypeof@1.1.1:
334 | version "1.1.1"
335 | resolved "https://registry.nlark.com/setprototypeof/download/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
336 | integrity sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM=
337 |
338 | setprototypeof@1.2.0:
339 | version "1.2.0"
340 | resolved "https://registry.nlark.com/setprototypeof/download/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
341 | integrity sha1-ZsmiSnP5/CjL5msJ/tPTPcrxtCQ=
342 |
343 | socket.io-adapter@~2.3.0:
344 | version "2.3.1"
345 | resolved "https://registry.nlark.com/socket.io-adapter/download/socket.io-adapter-2.3.1.tgz#a442720cb09a4823cfb81287dda1f9b52d4ccdb2"
346 | integrity sha1-pEJyDLCaSCPPuBKH3aH5tS1MzbI=
347 |
348 | socket.io-parser@~4.0.3:
349 | version "4.0.4"
350 | resolved "https://registry.npm.taobao.org/socket.io-parser/download/socket.io-parser-4.0.4.tgz?cache=0&sync_timestamp=1610669809014&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsocket.io-parser%2Fdownload%2Fsocket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
351 | integrity sha1-nqIbDWFQjRgZbvBKLGuatjD0wrA=
352 | dependencies:
353 | "@types/component-emitter" "^1.2.10"
354 | component-emitter "~1.3.0"
355 | debug "~4.3.1"
356 |
357 | socket.io@^4.1.2:
358 | version "4.1.2"
359 | resolved "https://registry.nlark.com/socket.io/download/socket.io-4.1.2.tgz#f90f9002a8d550efe2aa1d320deebb9a45b83233"
360 | integrity sha1-+Q+QAqjVUO/iqh0yDe67mkW4MjM=
361 | dependencies:
362 | "@types/cookie" "^0.4.0"
363 | "@types/cors" "^2.8.8"
364 | "@types/node" ">=10.0.0"
365 | accepts "~1.3.4"
366 | base64id "~2.0.0"
367 | debug "~4.3.1"
368 | engine.io "~5.1.0"
369 | socket.io-adapter "~2.3.0"
370 | socket.io-parser "~4.0.3"
371 |
372 | "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
373 | version "1.5.0"
374 | resolved "https://registry.nlark.com/statuses/download/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
375 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
376 |
377 | toidentifier@1.0.0:
378 | version "1.0.0"
379 | resolved "https://registry.nlark.com/toidentifier/download/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
380 | integrity sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM=
381 |
382 | tsscmp@1.0.6:
383 | version "1.0.6"
384 | resolved "https://registry.npm.taobao.org/tsscmp/download/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
385 | integrity sha1-hbmVg6w1iexL/vgltQAKqRHWBes=
386 |
387 | type-is@^1.6.16:
388 | version "1.6.18"
389 | resolved "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
390 | integrity sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=
391 | dependencies:
392 | media-typer "0.3.0"
393 | mime-types "~2.1.24"
394 |
395 | vary@^1, vary@^1.1.2:
396 | version "1.1.2"
397 | resolved "https://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
398 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
399 |
400 | ws@~7.4.2:
401 | version "7.4.6"
402 | resolved "https://registry.nlark.com/ws/download/ws-7.4.6.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fws%2Fdownload%2Fws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
403 | integrity sha1-VlTKjs3u5HwzqaS/bSjivimAN3w=
404 |
405 | ylru@^1.2.0:
406 | version "1.2.1"
407 | resolved "https://registry.npm.taobao.org/ylru/download/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
408 | integrity sha1-9Xa2M0FUeYnB3nuiiHYJI7J/6E8=
409 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
22 |
23 |
33 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/tetris-vue3/f65735fbcc1223686a65af1e64a91e045f09f45c/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/Box.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
23 |
40 |
--------------------------------------------------------------------------------
/src/components/Game.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/src/game/Box.js:
--------------------------------------------------------------------------------
1 | import { rotate, rotate270 } from "./matrix";
2 | export class Box {
3 | constructor(options = {}) {
4 | this._x = options.x || 0;
5 | this._y = options.y || 0;
6 | this._type = options.type || "";
7 | this._shape = options.shape || [
8 | [2, 0, 0],
9 | [2, 2, 0],
10 | [0, 2, 0],
11 | ];
12 | this._rotateIndex = 0;
13 | this._rotateStrategy = [];
14 | }
15 |
16 | setRotateStrategy(strategy) {
17 | if (strategy) {
18 | this._rotateStrategy = strategy;
19 | }
20 | }
21 |
22 | rotate() {
23 | const rotateFn = this._rotateStrategy[this._rotateIndex];
24 | this.shape = rotateFn(this.shape);
25 | this._rotateIndex = this.nextRotateIndex();
26 | }
27 |
28 | nextRotateIndex() {
29 | let index = this._rotateIndex;
30 |
31 | index++;
32 | if (index >= this._rotateStrategy.length) index = 0;
33 |
34 | return index;
35 | }
36 |
37 | peerNextRotateShape() {
38 | const rotateFn = this._rotateStrategy[this.nextRotateIndex()];
39 | return rotateFn(this.shape);
40 | }
41 |
42 | get x() {
43 | return this._x;
44 | }
45 |
46 | set x(val) {
47 | this._x = val;
48 | }
49 |
50 | get y() {
51 | return this._y;
52 | }
53 |
54 | set y(val) {
55 | this._y = val;
56 | }
57 |
58 | get type() {
59 | return this._type;
60 | }
61 |
62 | get shape() {
63 | return this._shape;
64 | }
65 |
66 | set shape(val) {
67 | this._shape = val;
68 | }
69 | }
70 |
71 | export function createBox({ x, y, shape, type } = {}) {
72 | return new Box({ x, y, shape, type });
73 | }
74 |
75 | export function randomCreateBox() {
76 | const { shape, rotateStrategy, type } = randomGenerateShape();
77 |
78 | const box = createBox({ shape, type, y: -1 });
79 | box.setRotateStrategy(rotateStrategy);
80 |
81 | return box;
82 | }
83 |
84 | const boxsInfo = {
85 | 0: {
86 | type: 0,
87 | shape: [
88 | [1, 1],
89 | [1, 1],
90 | ],
91 | },
92 |
93 | 1: {
94 | type: 1,
95 | shape: [
96 | [0, 1, 1],
97 | [1, 1, 0],
98 | [0, 0, 0],
99 | ],
100 | rotateStrategy: [rotate, rotate270],
101 | },
102 |
103 | 2: {
104 | type: 2,
105 | shape: [
106 | [5, 5, 5],
107 | [0, 5, 0],
108 | [0, 0, 0],
109 | ],
110 | rotateStrategy: [rotate, rotate, rotate, rotate],
111 | },
112 |
113 | 3: {
114 | type: 3,
115 | shape: [
116 | [0, 7, 0, 0],
117 | [0, 7, 0, 0],
118 | [0, 7, 0, 0],
119 | [0, 7, 0, 0],
120 | ],
121 | rotateStrategy: [rotate, rotate270],
122 | },
123 | 4: {
124 | type: 4,
125 | shape: [
126 | [4, 0, 0],
127 | [4, 0, 0],
128 | [4, 4, 0],
129 | ],
130 | rotateStrategy: [rotate, rotate, rotate, rotate],
131 | },
132 |
133 | 5: {
134 | type: 5,
135 | shape: [
136 | [0, 0, 6],
137 | [0, 0, 6],
138 | [0, 6, 6],
139 | ],
140 | rotateStrategy: [rotate, rotate, rotate, rotate],
141 | },
142 | };
143 |
144 | function randomGenerateShape() {
145 | const len = Object.keys(boxsInfo).length - 1;
146 | const index = Math.ceil(Math.random() * len);
147 |
148 | return boxsInfo[index];
149 | }
150 |
151 | export function getBoxsInfoByKey(key) {
152 | return boxsInfo[key];
153 | }
154 |
--------------------------------------------------------------------------------
/src/game/Game.js:
--------------------------------------------------------------------------------
1 | // 游戏场景
2 | import { addTicker, removeTicker } from "./ticker";
3 | import {
4 | hitRightBox,
5 | hitLeftBox,
6 | hitRightBoundary,
7 | hitLeftBoundary,
8 | hitBottomBox,
9 | hitBottomBoundary,
10 | } from "./hit";
11 | import { createBox } from "./Box";
12 | import { lineElimination } from "./eliminate";
13 | import { render } from "./renderer";
14 | import { addToMap, initMap, addOneLineToMap, checkLegalBoxInMap } from "./map";
15 | import { StateManagement } from "./StateManagement.js";
16 | import mitt from "mitt";
17 | export class Game {
18 | constructor(map) {
19 | this._map = map;
20 | this._activeBox = null;
21 | this._player = null;
22 | this._emitter = mitt();
23 | this._autoMoveToDown = true;
24 | this._stateManagement = new StateManagement();
25 | initMap(this._map);
26 | }
27 |
28 | start() {
29 | this._player.init();
30 | addTicker(this.handleTicker, this);
31 | }
32 |
33 | addPlayer(player) {
34 | this._player = player;
35 | this._player.addGame(this);
36 | }
37 |
38 | setCreateBoxStrategy(strategy) {
39 | this._createBoxStrategy = strategy;
40 | }
41 |
42 | set autoMoveToDown(val) {
43 | this._autoMoveToDown = val;
44 | }
45 |
46 | handleTicker(i) {
47 | this.handleAutoMoveToDown(i);
48 | render(this._activeBox, this._map);
49 | }
50 |
51 | _n = 0;
52 | handleAutoMoveToDown(i) {
53 | if (!this._autoMoveToDown) return;
54 | this._n += i;
55 | if (this._n >= this.getSpeed()) {
56 | this._n = 0;
57 | this.moveBoxToDown();
58 | this._emitter.emit("autoMoveToDown");
59 | }
60 | }
61 |
62 | nextBox(activeBox) {
63 | addToMap(activeBox, this._map);
64 | const num = lineElimination(this._map);
65 | // 通知消除的行数
66 | this._emitter.emit("eliminateLine", num);
67 | // 检测是不是游戏结束了
68 | if (this.checkGameOver()) {
69 | this._emitter.emit("gameOver");
70 | return;
71 | }
72 | this.addBox();
73 | }
74 |
75 | endGame() {
76 | removeTicker(this.handleTicker, this);
77 | this._emitter.all.clear();
78 | }
79 |
80 | checkGameOver() {
81 | // 需要在新的 box 来之前检测
82 | return this._activeBox.y < 0;
83 | }
84 |
85 | addBox() {
86 | this._activeBox = this._createBoxStrategy();
87 | }
88 |
89 | resetSpeed() {
90 | this._stateManagement.resetSpeed();
91 | }
92 |
93 | speedUp() {
94 | this._stateManagement.speedUp();
95 | }
96 |
97 | moveBoxToDown() {
98 | if (!this._activeBox) return;
99 | if (
100 | hitBottomBoundary(this._activeBox, this._map) ||
101 | hitBottomBox(this._activeBox, this._map)
102 | ) {
103 | this.nextBox(this._activeBox);
104 | return;
105 | }
106 | this._activeBox.y++;
107 | }
108 |
109 | moveBoxToLeft() {
110 | if (
111 | hitLeftBoundary(this._activeBox, this._map) ||
112 | hitLeftBox(this._activeBox, this._map)
113 | ) {
114 | return;
115 | }
116 |
117 | this._activeBox.x--;
118 | }
119 |
120 | moveBoxToRight() {
121 | if (
122 | hitRightBoundary(this._activeBox, this._map) ||
123 | hitRightBox(this._activeBox, this._map)
124 | ) {
125 | return;
126 | }
127 |
128 | this._activeBox.x++;
129 | }
130 |
131 | rotateBox() {
132 | const box = createBox({
133 | x: this._activeBox.x,
134 | y: this._activeBox.y,
135 | shape: this._activeBox.peerNextRotateShape(),
136 | });
137 |
138 | if (checkLegalBoxInMap(box, this._map)) {
139 | return;
140 | }
141 |
142 | this._activeBox.rotate();
143 | }
144 |
145 | getSpeed() {
146 | return this._stateManagement.speed;
147 | }
148 |
149 | addOneLine() {
150 | addOneLineToMap(this._map);
151 | }
152 |
153 | get emitter() {
154 | return this._emitter;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/game/Player.js:
--------------------------------------------------------------------------------
1 | import { randomCreateBox } from "./Box";
2 | import { socket } from "../utils/socket";
3 |
4 | export class Player {
5 | constructor() {
6 | this._game = null;
7 | socket.on("addLine", this.addLine.bind(this));
8 | socket.on("gameWon", this.gameWon.bind(this));
9 | }
10 |
11 | addGame(game) {
12 | this._game = game;
13 | this._game.setCreateBoxStrategy(this.createBoxStrategy.bind(this));
14 | this._game.emitter.on("eliminateLine", this.handleEliminateLine.bind(this));
15 | this._game.emitter.on("gameOver", this.gameOver.bind(this));
16 | this._game.emitter.on("autoMoveToDown", this.autoMoveToDown.bind(this));
17 | }
18 |
19 | init() {
20 | this.initKeyboard();
21 | // 初始化的时候,让 game 开始掉落 box
22 | this._game.addBox();
23 | }
24 |
25 | autoMoveToDown() {
26 | socket.emit("moveBoxToDown");
27 | }
28 |
29 | gameWon() {
30 | alert("You Won !!!");
31 | this._game.endGame();
32 | }
33 |
34 | gameOver() {
35 | alert("game over , You lose !!!");
36 | socket.emit("gameOver", "lose");
37 | this._game.endGame();
38 | }
39 |
40 | handleEliminateLine(num) {
41 | socket.emit("eliminateLine", num);
42 | }
43 |
44 | addLine(num) {
45 | // 别人消行了,这里就需要添加一行
46 | for (let i = 0; i < num; i++) {
47 | this._game.addOneLine();
48 | }
49 | }
50 |
51 | createBoxStrategy() {
52 | const box = randomCreateBox();
53 |
54 | socket.emit("createBox", {
55 | x: box.x,
56 | y: box.y,
57 | type: box.type,
58 | });
59 |
60 | return box;
61 | }
62 |
63 | initKeyboard() {
64 | window.addEventListener("keyup", this.handleKeyup.bind(this));
65 | window.addEventListener("keydown", this.handleKeydown.bind(this));
66 | }
67 |
68 | handleKeyup(e) {
69 | if (e.code === "ArrowDown") {
70 | this._game.resetSpeed();
71 | }
72 | }
73 |
74 | handleKeydown(e) {
75 | switch (e.code) {
76 | case "ArrowRight":
77 | this._game.moveBoxToRight();
78 | socket.emit("moveBoxToRight");
79 | break;
80 | case "ArrowLeft":
81 | this._game.moveBoxToLeft();
82 | socket.emit("moveBoxToLeft");
83 | break;
84 | case "ArrowDown":
85 | this._game.speedUp();
86 | break;
87 | case "Space":
88 | this._game.rotateBox();
89 | socket.emit("rotateBox");
90 | break;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/game/Rival.js:
--------------------------------------------------------------------------------
1 | // 对手
2 | // 需要实现以下几个接口
3 | //
4 | // 1. 向下移动
5 | // 2. 向左移动
6 | // 3. 向右移动
7 | // 4. 旋转
8 | // 5. 创建 box
9 | import { createBox } from "./Box";
10 | import { getBoxsInfoByKey } from "./Box";
11 | import { socket } from "../utils/socket";
12 | export class Rival {
13 | constructor() {
14 | this._game = null;
15 | this._boxInfo = null;
16 | this._isMounted = false;
17 | this.initSocketEvents();
18 | }
19 |
20 | addGame(game) {
21 | this._game = game;
22 | this._game.autoMoveToDown = false;
23 | this._game.setCreateBoxStrategy(this.createBoxStrategy.bind(this));
24 | }
25 |
26 | init() {
27 | console.log("Rival");
28 | }
29 |
30 | initSocketEvents() {
31 | socket.on("moveBoxToDown", this.moveBoxToDown.bind(this));
32 | socket.on("moveBoxToLeft", this.moveBoxToLeft.bind(this));
33 | socket.on("moveBoxToRight", this.moveBoxToRight.bind(this));
34 | socket.on("rotateBox", this.rotateBox.bind(this));
35 | socket.on("createBox", this.createBox.bind(this));
36 | socket.on("syncAddLine", this.syncAddLine.bind(this));
37 | }
38 |
39 | syncAddLine(num) {
40 | for (let i = 0; i < num; i++) {
41 | this._game.addOneLine();
42 | }
43 | }
44 |
45 | createBox(info) {
46 | this._boxInfo = info;
47 | if (!this._isMounted) {
48 | this._isMounted = true;
49 | // 主动触发 addBox 逻辑
50 | // 触发 addBox 的时候 game 会调用 createBoxStrategy 方法
51 | // 这样才会把 box 添加到 game 内
52 | this._game.addBox();
53 | }
54 | }
55 |
56 | moveBoxToLeft() {
57 | this._game.moveBoxToLeft();
58 | }
59 |
60 | moveBoxToRight() {
61 | this._game.moveBoxToRight();
62 | }
63 |
64 | rotateBox() {
65 | this._game.rotateBox();
66 | }
67 |
68 | moveBoxToDown() {
69 | this._game.moveBoxToDown();
70 | }
71 |
72 | createBoxStrategy() {
73 | const { shape, rotateStrategy } = getBoxsInfoByKey(this._boxInfo.type);
74 | const box = createBox({
75 | x: this._boxInfo.x,
76 | y: this._boxInfo.y,
77 | type: this._boxInfo.type,
78 | shape,
79 | });
80 |
81 | box.setRotateStrategy(rotateStrategy);
82 | return box;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/game/StateManagement.js:
--------------------------------------------------------------------------------
1 | // 游戏的状态管理
2 | // 比如游戏的速度
3 | // 游戏当前的积分等
4 | import { config } from "./config";
5 |
6 | export class StateManagement {
7 | constructor() {
8 | this._speed = 0;
9 | this.initSpeed();
10 | }
11 |
12 | initSpeed() {
13 | this._speed = config.game.speed;
14 | }
15 |
16 | speedUp() {
17 | this._speed = this._speed * config.game.speedFactor;
18 | if (this._speed <= config.game.speedMin) {
19 | this._speed = config.game.speedMin;
20 | }
21 | }
22 |
23 | resetSpeed() {
24 | this.initSpeed();
25 | }
26 |
27 | get speed() {
28 | return this._speed;
29 | }
30 |
31 | getState() {
32 | return {
33 | speed: this._speed,
34 | };
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/game/config.js:
--------------------------------------------------------------------------------
1 | export const config = {
2 | game: {
3 | row: 15,
4 | col: 10,
5 | speed: 1000,
6 | speedFactor: 0.6,
7 | speedMin: 30,
8 | },
9 | box: {
10 | width: 40,
11 | height: 40,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/game/eliminate.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * 消行
4 | * @param {} map
5 | * @returns 返回消除的行数
6 | */
7 | export function lineElimination(map) {
8 | // 1. 先把所有列都为 1 的行找出来 -》 得到一个索引数组
9 | // 2. 基于行的索引把
10 | const lines = canEliminationLines(map);
11 |
12 | // 需要先删除前面的,在删除后面的,防止数据的移动
13 | // 所有用得 reverse 来调换一下顺序
14 | const col = map[0].length;
15 |
16 | lines.reverse().forEach((line) => {
17 | map.splice(line, 1);
18 | map.unshift(Array(col).fill(0));
19 | });
20 |
21 | return lines.length;
22 | }
23 |
24 | // 得到的是索引
25 | export function canEliminationLines(map) {
26 | let result = [];
27 | const row = map.length;
28 | const col = map[0].length;
29 |
30 | for (let i = row - 1; i >= 0; i--) {
31 | let hit = true;
32 | for (let j = 0; j < col; j++) {
33 | if (map[i][j] !== -1) {
34 | hit = false;
35 | break;
36 | }
37 | }
38 |
39 | if (hit) {
40 | result.push(i);
41 | }
42 | }
43 |
44 | return result;
45 | }
46 |
--------------------------------------------------------------------------------
/src/game/hit.js:
--------------------------------------------------------------------------------
1 | import { getPointsHandler } from "./matrix";
2 |
3 | function _hitBox({ box, map, type, offsetX = 0, offsetY = 0 }) {
4 | const getPoints = getPointsHandler(type);
5 |
6 | return getPoints(box.shape).some((p) => {
7 | // 把 box 的坐标转换为 map 的 也就是全局的坐标,然后+上 offsetY 看看
8 | // 因为 point 都已经是有值得点了,所以不需要额外的判断
9 | const col = box.x + p.x;
10 | const row = box.y + p.y;
11 |
12 | return map[row + offsetY][col + offsetX] < 0;
13 | });
14 | }
15 |
16 | export function hitRightBox(box, map) {
17 | return _hitBox({
18 | box,
19 | map,
20 | type: "right",
21 | offsetX: 1,
22 | });
23 | }
24 |
25 | export function hitLeftBox(box, map) {
26 | return _hitBox({
27 | box,
28 | map,
29 | type: "left",
30 | offsetX: -1,
31 | });
32 | }
33 |
34 | export function hitBottomBox(box, map) {
35 | return _hitBox({
36 | box,
37 | map,
38 | type: "bottom",
39 | offsetY: 1,
40 | });
41 | }
42 |
43 | function hitBoundary({ box, map, type, offsetX = 0, offsetY = 0 }) {
44 | const getPoints = getPointsHandler(type);
45 |
46 | const mapRow = map.length;
47 | const mapCol = map[0].length;
48 |
49 | return getPoints(box.shape).some((p) => {
50 | const col = box.x + p.x + offsetX;
51 | const row = box.y + p.y + offsetY;
52 | // 如果这个 col 和 row 点 转换不了 map 里面的点的话,那么就说明这个点是超出屏幕了
53 |
54 | const checkCol = col < 0 || col >= mapCol;
55 | const checkRow = row < 0 || row >= mapRow;
56 |
57 | return checkCol || checkRow;
58 | });
59 | }
60 |
61 | export function hitLeftBoundary(box, map) {
62 | return hitBoundary({
63 | box,
64 | map,
65 | type: "left",
66 | offsetX: -1,
67 | });
68 | }
69 |
70 | export function hitRightBoundary(box, map) {
71 | return hitBoundary({
72 | box,
73 | map,
74 | type: "right",
75 | offsetX: 1,
76 | });
77 | }
78 |
79 | export function hitBottomBoundary(box, map) {
80 | return hitBoundary({
81 | box,
82 | map,
83 | type: "bottom",
84 | offsetY: 1,
85 | });
86 | }
87 |
--------------------------------------------------------------------------------
/src/game/index.js:
--------------------------------------------------------------------------------
1 | import { config } from "./config";
2 | import { Game } from "./Game";
3 | import { Player } from "./Player";
4 | import { Rival } from "./Rival";
5 |
6 | export const gameRow = config.game.row;
7 | export const gameCol = config.game.col;
8 |
9 | let selfGame = null;
10 |
11 | // 自己的游戏需要 start ,别人的不需要 start
12 | // 因为 dival 初始化要在 self 之前
13 |
14 | export function initSelfGame(map) {
15 | selfGame = new Game(map);
16 | selfGame.addPlayer(new Player());
17 | }
18 |
19 | export function initRivalGame(map) {
20 | const game = new Game(map);
21 | game.addPlayer(new Rival());
22 | // 初始化的时候就需要 start
23 | game.start();
24 | }
25 |
26 | export function startGame() {
27 | selfGame.start();
28 | }
29 |
--------------------------------------------------------------------------------
/src/game/map.js:
--------------------------------------------------------------------------------
1 | // 小于 0 的话是参与碰撞检测的
2 | // -1 的是可以消除的行
3 | // -2 的是不可以消除的行
4 |
5 | import { config } from "./config";
6 | export function initMap(map) {
7 | // init map
8 | for (let i = 0; i < config.game.row; i++) {
9 | map[i] = [];
10 | for (let j = 0; j < config.game.col; j++) {
11 | map[i][j] = 0;
12 | }
13 | }
14 | }
15 |
16 | export function addToMap(box, map) {
17 | const shape = box.shape;
18 |
19 | for (let i = 0; i < shape.length; i++) {
20 | for (let j = 0; j < shape[i].length; j++) {
21 | // 如果当前的这个位置已经被占用了,那么后来的就不可以被赋值
22 | if (checkLegalPointInMap({ x: j + box.x, y: i + box.y })) {
23 | if (shape[i][j]) {
24 | map[i + box.y][j + box.x] = -1;
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
31 | export function addOneLineToMap(map) {
32 | // 需要把所有为 -1 的值都往上移动一个位置
33 | // 1. 可以筛选出所有包含 -1 的 line 的 索引
34 | // - 找到最新的那个 line 的索引
35 | // 2. 删除这个 line ,这样的话,后面的 line 会补位过来
36 | // 3. 创建一个都是 -1 的line 添加都 map 的最后面
37 | // 4. 用 -2 来标记,这个是不可以消除的
38 |
39 | const row = map.length;
40 | const col = map[0].length;
41 | const getMinLine = () => {
42 | let r = 0;
43 | for (let i = 0; i < row; i++) {
44 | for (let j = 0; j < col; j++) {
45 | if (map[i][j] === -1) {
46 | // 获取当前在 line 的行的上一个 line
47 | return i - 1;
48 | }
49 | }
50 | }
51 |
52 | return r;
53 | };
54 |
55 | const minLine = getMinLine();
56 | if (minLine !== -1) {
57 | map.splice(minLine, 1);
58 | // -2 标记这行是不可以消除的
59 | map.push(Array(col).fill(-2));
60 | }
61 | }
62 |
63 | /**
64 | * 检测 point 是否可以再 map 中渲染
65 | * @param {} box
66 | * @returns boolean
67 | */
68 | export function checkLegalPointInMap(point) {
69 | const mapRow = config.game.row;
70 | const mapCol = config.game.col;
71 |
72 | const checkCol = point.x < 0 || point.x >= mapCol;
73 | const checkRow = point.y < 0 || point.y >= mapRow;
74 | return !checkCol && !checkRow;
75 | }
76 |
77 | export function checkLegalBoxInMap(box, map) {
78 | const shape = box.shape;
79 | const row = shape.length;
80 | const col = shape[0].length;
81 |
82 | for (let i = 0; i < row; i++) {
83 | for (let j = 0; j < col; j++) {
84 | const xx = box.x + j;
85 | const yy = box.y + i;
86 |
87 | if (!checkLegalPointInMap({ x: xx, y: yy })) return true;
88 | if (isHardPoint({ row: yy, col: xx, map })) return true;
89 | }
90 | }
91 |
92 | return false;
93 | }
94 |
95 | /**
96 | * 是不是硬点
97 | * 硬点指的是 type 为小于 0 的点
98 | * @param {} x
99 | * @param {*} y
100 | */
101 | export function isHardPoint({ row, col, map }) {
102 | return map[row][col] < 0;
103 | }
104 |
--------------------------------------------------------------------------------
/src/game/matrix.js:
--------------------------------------------------------------------------------
1 | export function getBottomPoints(matrix) {
2 | let result = [];
3 | const col = matrix[0].length;
4 | const row = matrix.length;
5 | for (let i = 0; i < col; i++) {
6 | for (let j = row - 1; j >= 0; j--) {
7 | const point = matrix[j][i];
8 | if (point) {
9 | result.push({ x: i, y: j });
10 | break;
11 | }
12 | }
13 | }
14 | return result;
15 | }
16 |
17 | export function getLeftPoints(matrix) {
18 | let result = [];
19 | const col = matrix[0].length;
20 | const row = matrix.length;
21 | for (let i = 0; i < row; i++) {
22 | for (let j = 0; j < col; j++) {
23 | if (matrix[i][j]) {
24 | result.push({
25 | x: j,
26 | y: i,
27 | });
28 | break;
29 | }
30 | }
31 | }
32 | return result;
33 | }
34 |
35 | export function getRightPoints(matrix) {
36 | let result = [];
37 | const col = matrix[0].length;
38 | const row = matrix.length;
39 |
40 | for (let i = 0; i < row; i++) {
41 | for (let j = col - 1; j >= 0; j--) {
42 | if (matrix[i][j]) {
43 | result.push({
44 | x: j,
45 | y: i,
46 | });
47 | break;
48 | }
49 | }
50 | }
51 | return result;
52 | }
53 |
54 | const mapFn = {
55 | left: getLeftPoints,
56 | right: getRightPoints,
57 | bottom: getBottomPoints,
58 | };
59 |
60 | export function getPointsHandler(direction) {
61 | return mapFn[direction];
62 | }
63 |
64 | export function rotate(matrix) {
65 | //逆时针旋转 90 度
66 | //列 = 行
67 | //行 = n - 1 - 列(j); n表示总行数
68 | var temp = [];
69 | var len = matrix.length;
70 | for (var i = 0; i < len; i++) {
71 | for (var j = 0; j < len; j++) {
72 | var k = len - 1 - j;
73 | if (!temp[k]) {
74 | temp[k] = [];
75 | }
76 | temp[k][i] = matrix[i][j];
77 | }
78 | }
79 |
80 | return temp;
81 | }
82 |
83 | export function rotate180(matrix) {
84 | //逆时针旋转 180 度
85 | //行 = h - 1 - 行(i); h表示总行数
86 | //列 = n - 1 - 列(j); n表示总列数
87 | var temp = [];
88 | var len = matrix.length;
89 | for (var i = 0; i < len; i++) {
90 | for (var j = 0; j < len; j++) {
91 | var k = len - 1 - i;
92 | if (!temp[k]) {
93 | temp[k] = [];
94 | }
95 | temp[k][len - 1 - j] = matrix[i][j];
96 | }
97 | }
98 |
99 | return temp;
100 | }
101 |
102 | export function rotate270(matrix) {
103 | //逆时针旋转 270 度
104 | //行 = 列
105 | //列 = n - 1 - 行(i); n表示总列数
106 | var temp = [];
107 | var len = matrix.length;
108 | for (var i = 0; i < len; i++) {
109 | for (var j = 0; j < len; j++) {
110 | var k = len - 1 - i;
111 | if (!temp[j]) {
112 | temp[j] = [];
113 | }
114 | temp[j][k] = matrix[i][j];
115 | }
116 | }
117 |
118 | return temp;
119 | }
120 |
--------------------------------------------------------------------------------
/src/game/renderer.js:
--------------------------------------------------------------------------------
1 | import { checkLegalPointInMap } from "./map";
2 | export function render(box, map) {
3 | if (!box) return;
4 | reset(map);
5 | _render(box, map);
6 | }
7 |
8 | function _render(box, map) {
9 | // 每次只重新 render active 的这个 box
10 | // 那些已经不动弹的 box 就不需要刷新了
11 | const shape = box.shape;
12 |
13 | for (let i = 0; i < shape.length; i++) {
14 | for (let j = 0; j < shape[i].length; j++) {
15 | // 如果当前的这个位置已经被占用了,那么后来的就不可以被赋值
16 | // 这个 shape 的 val 必须是有值得,才可以赋值给 map
17 | // 需要看看这个坐标是不是可以渲染(只可以渲染在 map 范围内的点)
18 | if (checkLegalPointInMap({ x: j + box.x, y: i + box.y })) {
19 | if (shape[i][j] && map[i + box.y][j + box.x] === 0) {
20 | map[i + box.y][j + box.x] = shape[i][j];
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | function reset(map) {
28 | const row = map.length;
29 | const col = map[0].length;
30 |
31 | for (let i = 0; i < row; i++) {
32 | for (let j = 0; j < col; j++) {
33 | if (map[i][j] >= 0) {
34 | map[i][j] = 0;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/game/ticker.js:
--------------------------------------------------------------------------------
1 | const tickers = [];
2 |
3 | // ticker
4 | let startTime = Date.now();
5 | function animate() {
6 | const interval = Date.now() - startTime;
7 |
8 | for (const ticker of tickers) {
9 | ticker.fn.call(ticker.listener, interval);
10 | }
11 |
12 | startTime = Date.now();
13 |
14 | requestAnimationFrame(animate);
15 | }
16 |
17 | requestAnimationFrame(animate);
18 |
19 | export function addTicker(fn, listener) {
20 | for (let i = 0; i < tickers.length; i++) {
21 | if (tickers[i].fn == fn && tickers[i].listener == listener) {
22 | return;
23 | }
24 | }
25 |
26 | tickers.push({
27 | fn,
28 | listener,
29 | });
30 | }
31 |
32 | export function removeTicker(fn, listener) {
33 | for (let i = 0; i < tickers.length; i++) {
34 | if (tickers[i].fn == fn && tickers[i].listener == listener) {
35 | tickers.splice(i, 1);
36 | }
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 | import { initSocket } from "./utils/socket";
4 |
5 | initSocket();
6 | createApp(App).mount("#app");
7 |
--------------------------------------------------------------------------------
/src/utils/socket.js:
--------------------------------------------------------------------------------
1 | // 负责处理消息
2 | import io from "socket.io-client";
3 |
4 | export let socket;
5 | export function initSocket() {
6 | socket = io("http://localhost:3001", {
7 | withCredentials: true,
8 | });
9 |
10 | // 连接成功
11 | socket.on("connect", () => {
12 | console.log("connect");
13 | });
14 | }
15 |
16 | export function clearSocket() {
17 | socket.off();
18 | }
19 |
--------------------------------------------------------------------------------
/tests/eliminate.spec.js:
--------------------------------------------------------------------------------
1 |
2 | import { lineElimination, canEliminationLines } from "../src/game/eliminate";
3 | describe("Line elimination", () => {
4 | it("消除第二行, 上面的行需要掉落下来", () => {
5 | const map = [
6 | [1, 0, 0, 0, 0],
7 | [-1, -1, -1, -1, -1],
8 | [0, 0, 0, 0, 0],
9 | [0, 0, 0, 0, 0],
10 | [0, 1, 0, 0, 0],
11 | ];
12 |
13 | lineElimination(map);
14 |
15 | const expectMap = [
16 | [0, 0, 0, 0, 0],
17 | [1, 0, 0, 0, 0],
18 | [0, 0, 0, 0, 0],
19 | [0, 0, 0, 0, 0],
20 | [0, 1, 0, 0, 0],
21 | ];
22 | expect(map).toEqual(expectMap);
23 | });
24 | });
25 |
26 | describe("canEliminationLines", () => {
27 | it("第二行是可以消除的", () => {
28 | const map = [
29 | [0, 0, 0, 0, 0],
30 | [-1, -1, -1, -1, -1],
31 | [0, 0, 0, 0, 0],
32 | [0, 0, 0, 0, 0],
33 | [0, -1, 0, 0, 0],
34 | ];
35 |
36 | const lines = canEliminationLines(map);
37 | expect(lines).toEqual([1]);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/tests/hit.spec.js:
--------------------------------------------------------------------------------
1 | import { hitBottomBox, hitLeftBox } from "../src/game/hit.js";
2 | import { Box } from "../src/game/Box";
3 |
4 | test("bottom", () => {
5 | const map = [
6 | [0, 0, 0, 0, 0],
7 | [0, 0, 0, 0, 0],
8 | [0, 0, 0, 0, 0],
9 | [0, 0, 0, 0, 0],
10 | [0, -1, 0, 0, 0],
11 | ];
12 |
13 | const box = new Box({ x: 0, y: 1 });
14 |
15 | box.shape = [
16 | [0, 0, 3],
17 | [0, 3, 3],
18 | [0, 3, 0],
19 | ];
20 |
21 | expect(hitBottomBox(box, map)).toBe(true);
22 | });
23 |
24 | describe("left", () => {
25 | it("not collision", () => {
26 | const map = [
27 | [0, 0, 0, 0, 0],
28 | [0, 0, 0, 0, 0],
29 | [0, 0, 0, 0, 0],
30 | [0, 0, 0, 0, 0],
31 | [0, -1, 0, 0, 0],
32 | ];
33 |
34 | const box = new Box({ x: 1, y: 0 });
35 | box.shape = [
36 | [2, 0, 0],
37 | [2, 2, 0],
38 | [0, 2, 0],
39 | ];
40 |
41 | expect(hitLeftBox(box, map)).toBe(false);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/tests/map.spec.js:
--------------------------------------------------------------------------------
1 | import { addOneLineToMap, checkLegalBoxInMap } from "../src/game/map";
2 | import { Box } from "../src/game/Box";
3 | describe("map", () => {
4 | describe("addOneLineToMap", () => {
5 | it("一个都没有的时候", () => {
6 | const map = [
7 | [0, 0, 0, 0],
8 | [0, 0, 0, 0],
9 | [0, 0, 0, 0],
10 | [0, 0, 0, 0],
11 | ];
12 |
13 | addOneLineToMap(map);
14 |
15 | expect(map).toEqual([
16 | [0, 0, 0, 0],
17 | [0, 0, 0, 0],
18 | [0, 0, 0, 0],
19 | [-2, -2, -2, -2],
20 | ]);
21 | });
22 | it("第一行有值", () => {
23 | const map = [
24 | [0, 0, 0, 0],
25 | [0, 0, 0, 0],
26 | [0, 0, 0, 0],
27 | [0, -2, 0, 0],
28 | ];
29 |
30 | addOneLineToMap(map);
31 |
32 | expect(map).toEqual([
33 | [0, 0, 0, 0],
34 | [0, 0, 0, 0],
35 | [0, -2, 0, 0],
36 | [-2, -2, -2, -2],
37 | ]);
38 | });
39 | it("在中间", () => {
40 | const map = [
41 | [0, 0, 0, 0],
42 | [0, 0, 0, 0],
43 | [0, -2, 0, 0],
44 | [0, -2, -2, 0],
45 | ];
46 |
47 | addOneLineToMap(map);
48 |
49 | expect(map).toEqual([
50 | [0, 0, 0, 0],
51 | [0, -2, 0, 0],
52 | [0, -2, -2, 0],
53 | [-2, -2, -2, -2],
54 | ]);
55 | });
56 | });
57 |
58 | describe("checkLegalBoxInMap", () => {
59 | test("right border", () => {
60 | const map = [
61 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
62 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
63 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
64 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
65 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
66 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
67 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
68 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
69 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
70 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
71 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
72 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
73 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
74 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
75 | [0, -1, 0, 0, 0, 0, -1, -1, -1, -1],
76 | ];
77 |
78 | const box = new Box({ x: 8, y: 5 });
79 | box.shape = [
80 | [0, 0, 0, 0],
81 | [7, 7, 7, 7],
82 | [0, 0, 0, 0],
83 | [0, 0, 0, 0],
84 | ];
85 |
86 | expect(checkLegalBoxInMap(box, map)).toBe(true);
87 | });
88 |
89 | test("left box", () => {
90 | const map = [
91 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
92 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
93 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
94 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
95 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
96 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
97 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
98 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
99 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
100 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
101 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
102 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
103 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
104 | [0, -1, 0, 0, 0, 0, 0, 0, 0, 0],
105 | [0, -1, 0, 0, 0, 0, -1, -1, -1, -1],
106 | ];
107 |
108 | const box = new Box({ x: 1, y: 5 });
109 | box.shape = [
110 | [0, 0, 0, 0],
111 | [7, 7, 7, 7],
112 | [0, 0, 0, 0],
113 | [0, 0, 0, 0],
114 | ];
115 |
116 | expect(checkLegalBoxInMap(box, map)).toBe(true);
117 | });
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/tests/matrix.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | getLeftPoints,
3 | getRightPoints,
4 | rotate,
5 | rotate180,
6 | rotate270,
7 | } from "../src/game/matrix";
8 | describe("matrix", () => {
9 | describe("获取边界点", () => {
10 | test("获取 matrix 左侧的边界点", () => {
11 | const matrix = [
12 | [0, 0, 3],
13 | [0, 3, 3],
14 | [0, 3, 0],
15 | ];
16 |
17 | expect(getLeftPoints(matrix)).toEqual([
18 | { x: 2, y: 0 },
19 | { x: 1, y: 1 },
20 | { x: 1, y: 2 },
21 | ]);
22 | });
23 |
24 | test("获取 matrix 右侧的边界点", () => {
25 | const matrix = [
26 | [0, 0, 3],
27 | [0, 3, 3],
28 | [0, 3, 0],
29 | ];
30 |
31 | expect(getRightPoints(matrix)).toEqual([
32 | { x: 2, y: 0 },
33 | { x: 2, y: 1 },
34 | { x: 1, y: 2 },
35 | ]);
36 | });
37 | });
38 |
39 | describe("Rotate", () => {
40 | it("rotate 逆时针90度旋转 ", () => {
41 | const matrix = [
42 | [0, 1, 1],
43 | [1, 1, 0],
44 | [0, 0, 0],
45 | ];
46 |
47 | // 90
48 | expect(rotate(matrix)).toEqual([
49 | [1, 0, 0],
50 | [1, 1, 0],
51 | [0, 1, 0],
52 | ]);
53 |
54 | // 180
55 | expect(rotate(rotate(matrix))).toEqual([
56 | [0, 0, 0],
57 | [0, 1, 1],
58 | [1, 1, 0],
59 | ]);
60 |
61 | // // 270
62 | expect(rotate(rotate(rotate(matrix)))).toEqual([
63 | [0, 1, 0],
64 | [0, 1, 1],
65 | [0, 0, 1],
66 | ]);
67 |
68 | // 0
69 | expect(rotate(rotate(rotate(rotate(matrix))))).toEqual([
70 | [0, 1, 1],
71 | [1, 1, 0],
72 | [0, 0, 0],
73 | ]);
74 | });
75 |
76 | it("逆时针旋转 180 度", () => {
77 | const matrix = [
78 | [0, 1, 1],
79 | [1, 1, 0],
80 | [0, 0, 0],
81 | ];
82 |
83 | expect(rotate180(matrix)).toEqual([
84 | [0, 0, 0],
85 | [0, 1, 1],
86 | [1, 1, 0],
87 | ]);
88 |
89 | expect(rotate180(rotate180(matrix))).toEqual([
90 | [0, 1, 1],
91 | [1, 1, 0],
92 | [0, 0, 0],
93 | ]);
94 | });
95 |
96 | it("逆时针旋转 270 度", () => {
97 | const matrix = [
98 | [0, 1, 1],
99 | [1, 1, 0],
100 | [0, 0, 0],
101 | ];
102 |
103 | expect(rotate270(matrix)).toEqual([
104 | [0, 1, 0],
105 | [0, 1, 1],
106 | [0, 0, 1],
107 | ]);
108 | });
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()]
7 | })
8 |
--------------------------------------------------------------------------------