├── .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 | ![](https://api2.mubu.com/v3/document_image/84c479b6-f86f-4887-9e33-dacaeaf27d0b-7425747.jpg) 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 | 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 | 6 | 7 | 23 | 40 | -------------------------------------------------------------------------------- /src/components/Game.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------