├── .gitignore ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── pont-client ├── .gitignore ├── .htaccess ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── deploy │ ├── favicon.ico │ ├── index.html │ ├── pont_client.js │ ├── pont_client_bg.wasm │ ├── rules.html │ └── style.css └── src │ └── lib.rs ├── pont-common ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── pont-server ├── Cargo.lock ├── Cargo.toml └── src │ ├── main.rs │ └── words.txt └── pont.conf /.gitignore: -------------------------------------------------------------------------------- 1 | */target 2 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matthew Keeter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | ### [Project homepage](https://mattkeeter.com/projects/pont) 3 | 4 | `pont` is an online game based on 5 | [Qwirkle (by Mindware Games)](https://en.wikipedia.org/wiki/Qwirkle) 6 | 7 | ![Screenshot](https://mattkeeter.com/projects/pont/screenshot.png) 8 | 9 | Notably, both the client and server are written in Rust; 10 | the only Javascript is a shim to load the WebAssembly module. 11 | 12 | # Hosting 13 | It's easiest to run the whole application on a single VM, 14 | using [NGINX](https://www.nginx.com/) to both serve static content 15 | and to act as a secure proxy for websocket communication. 16 | The latter means we don't need SSL support in the game server itself. 17 | 18 | The system looks something like this: 19 | 20 | ![Screenshot](https://mattkeeter.com/projects/pont/diagram.svg) 21 | 22 | I'm hosting a copy of the game at 23 | [https://pont.mattkeeter.com](https://pont.mattkeeter.com), 24 | using a $5/month droplet from [Digital Ocean](https://www.digitalocean.com/) 25 | and [Dreamhost](https://www.dreamhost.com/) for domain registration. 26 | 27 | ## Initial setup 28 | ``` 29 | sudo apt update 30 | sudo apt install build-essentials libssl-dev pkg-config 31 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 32 | ``` 33 | 34 | ## Installing NGINX and setting up Let's Encrypt 35 | ``` 36 | sudo apt install nginx 37 | sudo apt-get install software-properties-common 38 | sudo add-apt-repository universe 39 | sudo add-apt-repository ppa:certbot/certbot 40 | sudo apt-get update 41 | sudo apt-get install certbot python3-certbot-nginx 42 | 43 | sudo certbot --nginx 44 | ``` 45 | (read and follow `certbot`'s instructions) 46 | 47 | ## Turn on a firewall to improve security 48 | ``` 49 | sudo ufw allow ssh 50 | sudo ufw allow http 51 | sudo ufw allow https 52 | sudo ufw allow 8081 53 | sudo ufw enable 54 | ``` 55 | 56 | ## Building the client WebAssembly file 57 | ``` 58 | git clone https://github.com/mkeeter/pont.git 59 | cd pont/pont-client 60 | wasm-pack build --target web 61 | ``` 62 | 63 | ## Deploy the nginx config 64 | ``` 65 | sudo cp pont.conf /etc/nginx/sites-enabled/pont.conf 66 | sudo rm /etc/nginx/sites-enabled/default 67 | sudo nginx -s reload 68 | ``` 69 | This won't work out of the box, because the configuration assumes the url is 70 | `pont.mattkeeter.com`, which won't be true for you; edit it accordingly. 71 | 72 | ## Running the server 73 | ``` 74 | cd pont/pont-server 75 | cargo run --release 76 | ``` 77 | (leave this in a `screen` session for easy persistence!) 78 | 79 | # License 80 | © 2020 [Matthew Keeter](https://mattkeeter.com) 81 | 82 | Licensed under either of 83 | 84 | * Apache License, Version 2.0 85 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 86 | * MIT license 87 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 88 | 89 | at your option. 90 | 91 | ## Contribution 92 | 93 | Unless you explicitly state otherwise, any contribution intentionally submitted 94 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 95 | dual licensed as above, without any additional terms or conditions. 96 | -------------------------------------------------------------------------------- /pont-client/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeeter/pont/385e532ace2ef266f9c937621158e76c6f8d86d6/pont-client/.gitignore -------------------------------------------------------------------------------- /pont-client/.htaccess: -------------------------------------------------------------------------------- 1 | Header set Cache-Control "no-cache, must-revalidate, public" 2 | -------------------------------------------------------------------------------- /pont-client/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /pont-client/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "autocfg" 5 | version = "1.0.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 8 | 9 | [[package]] 10 | name = "bincode" 11 | version = "1.2.1" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" 14 | dependencies = [ 15 | "byteorder", 16 | "serde", 17 | ] 18 | 19 | [[package]] 20 | name = "bitflags" 21 | version = "1.2.1" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 24 | 25 | [[package]] 26 | name = "bumpalo" 27 | version = "3.2.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" 30 | 31 | [[package]] 32 | name = "byteorder" 33 | version = "1.3.4" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 36 | 37 | [[package]] 38 | name = "cfg-if" 39 | version = "0.1.10" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 42 | 43 | [[package]] 44 | name = "chrono" 45 | version = "0.4.11" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" 48 | dependencies = [ 49 | "num-integer", 50 | "num-traits", 51 | "time", 52 | ] 53 | 54 | [[package]] 55 | name = "console_error_panic_hook" 56 | version = "0.1.6" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" 59 | dependencies = [ 60 | "cfg-if", 61 | "wasm-bindgen", 62 | ] 63 | 64 | [[package]] 65 | name = "getrandom" 66 | version = "0.1.14" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 69 | dependencies = [ 70 | "cfg-if", 71 | "libc", 72 | "wasi", 73 | ] 74 | 75 | [[package]] 76 | name = "js-sys" 77 | version = "0.3.37" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055" 80 | dependencies = [ 81 | "wasm-bindgen", 82 | ] 83 | 84 | [[package]] 85 | name = "lazy_static" 86 | version = "1.4.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 89 | 90 | [[package]] 91 | name = "libc" 92 | version = "0.2.69" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 95 | 96 | [[package]] 97 | name = "log" 98 | version = "0.4.8" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 101 | dependencies = [ 102 | "cfg-if", 103 | ] 104 | 105 | [[package]] 106 | name = "num-integer" 107 | version = "0.1.42" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 110 | dependencies = [ 111 | "autocfg", 112 | "num-traits", 113 | ] 114 | 115 | [[package]] 116 | name = "num-traits" 117 | version = "0.2.11" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 120 | dependencies = [ 121 | "autocfg", 122 | ] 123 | 124 | [[package]] 125 | name = "pont-client" 126 | version = "0.1.0" 127 | dependencies = [ 128 | "bincode", 129 | "console_error_panic_hook", 130 | "js-sys", 131 | "lazy_static", 132 | "pont-common", 133 | "vergen", 134 | "wasm-bindgen", 135 | "web-sys", 136 | ] 137 | 138 | [[package]] 139 | name = "pont-common" 140 | version = "0.1.0" 141 | dependencies = [ 142 | "rand", 143 | "serde", 144 | "serde_derive", 145 | ] 146 | 147 | [[package]] 148 | name = "ppv-lite86" 149 | version = "0.2.6" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 152 | 153 | [[package]] 154 | name = "proc-macro2" 155 | version = "1.0.10" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 158 | dependencies = [ 159 | "unicode-xid", 160 | ] 161 | 162 | [[package]] 163 | name = "quote" 164 | version = "1.0.3" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 167 | dependencies = [ 168 | "proc-macro2", 169 | ] 170 | 171 | [[package]] 172 | name = "rand" 173 | version = "0.7.3" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 176 | dependencies = [ 177 | "getrandom", 178 | "libc", 179 | "rand_chacha", 180 | "rand_core", 181 | "rand_hc", 182 | ] 183 | 184 | [[package]] 185 | name = "rand_chacha" 186 | version = "0.2.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 189 | dependencies = [ 190 | "ppv-lite86", 191 | "rand_core", 192 | ] 193 | 194 | [[package]] 195 | name = "rand_core" 196 | version = "0.5.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 199 | dependencies = [ 200 | "getrandom", 201 | ] 202 | 203 | [[package]] 204 | name = "rand_hc" 205 | version = "0.2.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 208 | dependencies = [ 209 | "rand_core", 210 | ] 211 | 212 | [[package]] 213 | name = "serde" 214 | version = "1.0.106" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 217 | dependencies = [ 218 | "serde_derive", 219 | ] 220 | 221 | [[package]] 222 | name = "serde_derive" 223 | version = "1.0.106" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 226 | dependencies = [ 227 | "proc-macro2", 228 | "quote", 229 | "syn", 230 | ] 231 | 232 | [[package]] 233 | name = "syn" 234 | version = "1.0.17" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 237 | dependencies = [ 238 | "proc-macro2", 239 | "quote", 240 | "unicode-xid", 241 | ] 242 | 243 | [[package]] 244 | name = "time" 245 | version = "0.1.43" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 248 | dependencies = [ 249 | "libc", 250 | "winapi", 251 | ] 252 | 253 | [[package]] 254 | name = "unicode-xid" 255 | version = "0.2.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 258 | 259 | [[package]] 260 | name = "vergen" 261 | version = "3.1.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "4ce50d8996df1f85af15f2cd8d33daae6e479575123ef4314a51a70a230739cb" 264 | dependencies = [ 265 | "bitflags", 266 | "chrono", 267 | ] 268 | 269 | [[package]] 270 | name = "wasi" 271 | version = "0.9.0+wasi-snapshot-preview1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 274 | 275 | [[package]] 276 | name = "wasm-bindgen" 277 | version = "0.2.60" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f" 280 | dependencies = [ 281 | "cfg-if", 282 | "wasm-bindgen-macro", 283 | ] 284 | 285 | [[package]] 286 | name = "wasm-bindgen-backend" 287 | version = "0.2.60" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd" 290 | dependencies = [ 291 | "bumpalo", 292 | "lazy_static", 293 | "log", 294 | "proc-macro2", 295 | "quote", 296 | "syn", 297 | "wasm-bindgen-shared", 298 | ] 299 | 300 | [[package]] 301 | name = "wasm-bindgen-macro" 302 | version = "0.2.60" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4" 305 | dependencies = [ 306 | "quote", 307 | "wasm-bindgen-macro-support", 308 | ] 309 | 310 | [[package]] 311 | name = "wasm-bindgen-macro-support" 312 | version = "0.2.60" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931" 315 | dependencies = [ 316 | "proc-macro2", 317 | "quote", 318 | "syn", 319 | "wasm-bindgen-backend", 320 | "wasm-bindgen-shared", 321 | ] 322 | 323 | [[package]] 324 | name = "wasm-bindgen-shared" 325 | version = "0.2.60" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639" 328 | 329 | [[package]] 330 | name = "web-sys" 331 | version = "0.3.37" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb" 334 | dependencies = [ 335 | "js-sys", 336 | "wasm-bindgen", 337 | ] 338 | 339 | [[package]] 340 | name = "winapi" 341 | version = "0.3.8" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 344 | dependencies = [ 345 | "winapi-i686-pc-windows-gnu", 346 | "winapi-x86_64-pc-windows-gnu", 347 | ] 348 | 349 | [[package]] 350 | name = "winapi-i686-pc-windows-gnu" 351 | version = "0.4.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 354 | 355 | [[package]] 356 | name = "winapi-x86_64-pc-windows-gnu" 357 | version = "0.4.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 360 | -------------------------------------------------------------------------------- /pont-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pont-client" 3 | version = "0.1.0" 4 | authors = ["Matt Keeter"] 5 | edition = "2018" 6 | build = "build.rs" 7 | license = "MIT OR Apache-2.0" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | pont-common = { path = "../pont-common" } 14 | bincode = "*" 15 | wasm-bindgen = "^0.2" 16 | js-sys = "*" 17 | console_error_panic_hook = "*" 18 | lazy_static = "*" 19 | 20 | [dependencies.web-sys] 21 | version = "*" 22 | features = [ 23 | 'AddEventListenerOptions', 24 | 'Blob', 25 | 'console', 26 | 'Document', 27 | 'DomTokenList', 28 | 'Element', 29 | 'EventTarget', 30 | 'FileReader', 31 | 'HtmlCollection', 32 | 'HtmlElement', 33 | 'HtmlButtonElement', 34 | 'HtmlInputElement', 35 | 'KeyboardEvent', 36 | 'Location', 37 | 'MessageEvent', 38 | 'Node', 39 | 'NodeList', 40 | 'Performance', 41 | 'PointerEvent', 42 | 'ProgressEvent', 43 | 'SvgElement', 44 | 'SvgGraphicsElement', 45 | 'SvgMatrix', 46 | 'WebSocket', 47 | 'Window', 48 | ] 49 | 50 | [build-dependencies] 51 | vergen = "3" 52 | -------------------------------------------------------------------------------- /pont-client/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let flags = vergen::ConstantsFlags::all(); 3 | vergen::generate_cargo_keys(flags) 4 | .expect("Unable to generate the cargo keys!"); 5 | } 6 | -------------------------------------------------------------------------------- /pont-client/deploy/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkeeter/pont/385e532ace2ef266f9c937621158e76c6f8d86d6/pont-client/deploy/favicon.ico -------------------------------------------------------------------------------- /pont-client/deploy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pont 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 26 | 27 | 28 |
29 |
30 |

Name:

31 |

Room:

32 |

33 |

Colorblind mode:

34 |
35 | 39 |
40 | 41 | 42 | 89 | 90 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /pont-client/deploy/pont_client.js: -------------------------------------------------------------------------------- 1 | ../pkg/pont_client.js -------------------------------------------------------------------------------- /pont-client/deploy/pont_client_bg.wasm: -------------------------------------------------------------------------------- 1 | ../pkg/pont_client_bg.wasm -------------------------------------------------------------------------------- /pont-client/deploy/rules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pont Rules 6 | 7 | 8 | 9 | 10 | 11 |

Rules

12 |

Joining a game

13 |

Gameplay is split into rooms, 14 | which are identified by a three-word code 15 | (assigned by the server).

16 |

To create a new room, enter your name and click "Create new room" 17 | (or press Enter). Your room's code will be in the top left; 18 | send it to your friends.

19 |

To join a room, enter your name and the code that your friend 20 | send you, then click "Join existing room" (or press Enter).

21 | 22 |

Taking your turn

23 |

On each turn, you may either place pieces on the board or exchange pieces with the bag.

24 |

Placing pieces

25 |

26 | Pieces must be placed so that each connected row or column shares either a shape or a color, without duplicates. 27 | In the example below, all tiles in the row are orange, and all tiles in the column are X shaped. 28 |

29 | 30 | 31 | 32 | 33 |

34 | Each move may only contribute to one connected row or column. 35 | The move below is invalid because the two pieces are not contributing to the same line. 36 |

37 | 38 | 39 | 40 | 41 | 42 |

There are six colors and six shapes, so each connected line 43 | cannot contain more than six pieces (since duplicates are not allowed).

44 | 45 |

Exchanging pieces

46 |

Exchanging pieces lets you swap up to six pieces. This ends your turn.

47 |

Near the end of the game, you may not be able to exchange your entire hand (if the bag has fewer than six pieces left)

48 | 49 |

Scoring

50 |

51 | Each move scores one point for each tile in a line that is touched by the move. 52 | The move below adds a tile, producing a line of length 4, 53 | and therefore scores 4 points. 54 |

55 | 56 | 57 | 58 | 59 | 60 |

It's possible for a move to contribute to more than one line. 61 | The move below scores 3 points for the horizontal (green) line, 62 | plus 2 points for the vertical (circles) line, for a total of 5 points. 63 |

64 | 65 | 66 | 67 | 68 | 69 |

Completing a line gives a 6-point bonus, 70 | in addition to the 6 points from the line itself. 71 | The move below is worth 12 points. 72 |

73 | 74 | 75 | 76 | 77 |

Ending the game

78 |

The game ends when there are no pieces left in the bag, 79 | and a player has emptied their hand.

80 | 81 |

The player who ends the game (by emptying their hand) 82 | scores a 6-point bonus.

83 | 84 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /pont-client/deploy/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark0: #1d2021; 3 | --dark1: #3c3836; 4 | --dark2: #504945; 5 | --dark3: #665c54; 6 | --dark4: #7c6f64; 7 | 8 | --light4: #a89984; 9 | --light3: #bdae93; 10 | --light2: #d5c4a1; 11 | --light1: #ebdbb2; 12 | --light0: #fbf1c7; 13 | 14 | --yellow: #ff5; 15 | --orange: #fa5; 16 | --violet: #c7f; 17 | --blue: #5cf; 18 | --green: #5e5; 19 | --red: #f55; 20 | --dark-red: #922; 21 | } 22 | 23 | body { 24 | font-family: Lato, sans-serif; 25 | font-size: 16px; 26 | color: var(--dark2); 27 | margin-left: 20px; 28 | margin-right: 20px; 29 | background-color: var(--light1); 30 | } 31 | 32 | div.information { 33 | font-size: 20px; 34 | } 35 | 36 | input { 37 | font-family: Lato, sans-serif; 38 | font-size: 16px; 39 | background-color: var(--light0); 40 | border:1px solid var(--dark4); 41 | padding: 10px; 42 | margin-left: 10px; 43 | } 44 | 45 | /* Shows an error if you connect to an invalid room */ 46 | div#err_div { 47 | color: var(--red); 48 | } 49 | span#err_span { 50 | margin-left: 10px; 51 | } 52 | 53 | /* Make the 'Room:' text unselectable to make copy-pasting easier */ 54 | .noselect { 55 | -webkit-touch-callout: none; 56 | -webkit-user-select: none; 57 | -khtml-user-select: none; 58 | -moz-user-select: none; 59 | -ms-user-select: none; 60 | user-select: none; 61 | } 62 | 63 | /* Welcome to... the chat zone */ 64 | div#chat_input_div { 65 | display:flex; 66 | flex-direction: row; 67 | margin-top: 10px; 68 | } 69 | input#chat_input { 70 | min-width: 0px; 71 | flex: 1; 72 | } 73 | div#chat_name { 74 | padding-top: 10px; 75 | } 76 | div#chat_msgs { 77 | height: 100px; 78 | overflow-y: auto; 79 | border:1px solid var(--dark4); 80 | background-color: var(--light0); 81 | padding: 10px; 82 | } 83 | div#chat_div { 84 | display: flex; 85 | flex-direction: column; 86 | overflow-x: auto; 87 | } 88 | #chat_msgs span { 89 | margin-left: 5px; 90 | } 91 | p.msg { 92 | margin-top: 2px; 93 | margin-bottom: 2px; 94 | } 95 | 96 | /* Button to join the game */ 97 | button { 98 | color: var(--dark2); 99 | background-color: var(--light0); 100 | border:1px solid var(--dark4); 101 | border-radius: 5px; 102 | transition: border-radius 0.1s; 103 | } 104 | 105 | form button { 106 | border-radius: 5px; 107 | transition: border-radius 0.1s; 108 | padding: 10px 20px; 109 | font-family: Lato, sans-serif; 110 | font-size: 16px; 111 | } 112 | 113 | span#revhash { 114 | font-family: Inconsolata, "Courier New", monospace; 115 | } 116 | span#room_name { 117 | margin-left: 10px; 118 | font-family: Inconsolata, "Courier New", monospace; 119 | } 120 | 121 | form:invalid button { 122 | color: var(--dark4); 123 | background-color: var(--light1); 124 | } 125 | 126 | button.disabled { 127 | color: var(--dark4); 128 | background-color: var(--light1); 129 | pointer-events: none; 130 | } 131 | 132 | /* Only change button radius on hover for devices that support it 133 | * (otherwise, they get hovered on click, which is weird) */ 134 | @media (hover: hover) { 135 | form:not(:invalid) button:hover { 136 | border-radius: 15px; 137 | } 138 | button.gameplay:hover { 139 | border-radius: 15px; 140 | } 141 | } 142 | 143 | /* Main game div */ 144 | div#game { 145 | display: grid; 146 | column-gap: 20px; 147 | } 148 | 149 | /* List of players */ 150 | table#score_table { 151 | table-layout: fixed; 152 | width: 100%; 153 | border-collapse: collapse; 154 | } 155 | th { 156 | border-bottom: 1px solid var(--dark4); 157 | padding-bottom: 4px; 158 | } 159 | tr.player-row td { 160 | padding: 4px; 161 | } 162 | tr.player-row i { 163 | visibility: hidden; 164 | } 165 | tr.player-row.active i { 166 | visibility: visible; 167 | } 168 | tr.disconnected { 169 | color: var(--dark4); 170 | } 171 | 172 | /* Tiles on the board */ 173 | g.placed rect.tile { 174 | fill: var(--dark3); 175 | stroke-width: 0.5; 176 | stroke: var(--dark4); 177 | } 178 | /* Last played tile */ 179 | g.played rect.tile { 180 | fill: var(--dark0); 181 | stroke: var(--light4); 182 | } 183 | div#svg_div.nyt g.piece rect.tile { 184 | fill: var(--dark3); 185 | } 186 | g.piece rect.tile { 187 | fill: var(--dark1); 188 | stroke-width: 0.5; 189 | stroke: var(--dark4); 190 | } 191 | g.invalid rect.tile { 192 | fill: var(--dark-red); 193 | stroke: var(--red); 194 | } 195 | g.piece { 196 | pointer-events: auto; 197 | } 198 | rect.shadow { 199 | fill: var(--light1); 200 | stroke-width: 0.5; 201 | stroke: var(--light2); 202 | } 203 | rect#pan_rect { 204 | fill: none; 205 | pointer-events: all; 206 | } 207 | g.shape-orange .color { 208 | fill: var(--orange); 209 | } 210 | .shape-yellow .color { 211 | fill: var(--yellow); 212 | } 213 | g.shape-green .color { 214 | fill: var(--green); 215 | } 216 | g.shape-red .color { 217 | fill: var(--red); 218 | } 219 | g.shape-blue .color { 220 | fill: var(--blue); 221 | } 222 | g.shape-purple .color { 223 | fill: var(--violet); 224 | } 225 | 226 | .corner { 227 | visibility: hidden; 228 | } 229 | div.colorblind .corner { 230 | visibility: visible; 231 | } 232 | 233 | /* Buttons to submit your play */ 234 | button.gameplay { 235 | color: var(--dark1); 236 | position: absolute; 237 | height: 10%; 238 | width: 10%; 239 | font-size: 4vw; 240 | } 241 | button.gameplay:disabled { 242 | color: var(--dark4); 243 | background-color: var(--light1); 244 | pointer-events: none; 245 | } 246 | button#reject_button { 247 | left: 100%; 248 | top: 100%; 249 | transform: translateY(-100%) translateX(-100%); 250 | } 251 | button#accept_button { 252 | left: 100%; 253 | top: 100%; 254 | transform: translateY(-100%) translateX(-210%); 255 | } 256 | 257 | div#exchange_div { 258 | position: absolute; 259 | border:1px solid var(--dark4); 260 | background-color: var(--light0); 261 | height: 10%; 262 | width: 27.5%; 263 | top: calc(100%); 264 | left: 0%; 265 | transform: translateY(-100%) translateX(178%); 266 | 267 | -webkit-user-select: none; 268 | -ms-user-select: none; 269 | user-select: none; 270 | } 271 | div#exchange_div.disabled { 272 | color: var(--dark4); 273 | background-color: var(--light1); 274 | } 275 | div#svg_div.nyt div#exchange_div { 276 | color: var(--dark4); 277 | background-color: var(--light1); 278 | } 279 | 280 | div#exchange_div p { 281 | text-align: center; 282 | margin: 0; 283 | position: relative; 284 | top: 50%; 285 | transform: translateY(-50%); 286 | } 287 | div#count_div { 288 | padding-top: 4px; 289 | padding-bottom: 20px; 290 | } 291 | div#count_div p { 292 | color: var(--dark4); 293 | text-align: center; 294 | border-top: 1px solid var(--dark4); 295 | margin: 0px; 296 | padding-top: 5px; 297 | } 298 | 299 | /* This is only used for layout things */ 300 | svg#dummy { 301 | width: 100%; 302 | pointer-events: none; 303 | } 304 | 305 | /* Main game board */ 306 | svg#game_svg { 307 | position: absolute; 308 | width: 100%; 309 | height: 100%; 310 | left: 0px; 311 | top: 0px; 312 | pointer-events: none; 313 | } 314 | 315 | /* Background divs to create nice sharp rectangles */ 316 | div.background { 317 | position: absolute; 318 | border:1px solid var(--dark4); 319 | background-color: var(--light0); 320 | } 321 | div#hand { 322 | left: 0px; 323 | top: calc(100%); 324 | transform: translateY(-100%); 325 | width: calc(47.5%); 326 | height: 10%; 327 | } 328 | div#board { 329 | left: 0px; 330 | top: 0px; 331 | width: calc(100%); 332 | height: calc(87.5%); 333 | } 334 | 335 | div#svg_div { 336 | margin-bottom: 10px; 337 | position: relative; 338 | touch-action: none; 339 | } 340 | 341 | /* Two-column layout */ 342 | @media only screen and (min-width: 750px) { 343 | div#game { 344 | grid-template-columns: 2fr 1fr; 345 | } 346 | div#chat_div { 347 | grid-column: 1 / span 2; 348 | } 349 | button.gameplay { 350 | font-size: 3vw; 351 | } 352 | } 353 | 354 | /* Three-column layout */ 355 | @media only screen and (min-width: 1200px) { 356 | div#svg_div { 357 | margin-bottom: 0px; 358 | } 359 | button.gameplay { 360 | font-size: 2vw; 361 | } 362 | div#game { 363 | grid-template-columns: 2fr 1fr 1fr; 364 | } 365 | div#chat_msgs { 366 | display:flex; 367 | flex-direction: column; 368 | flex: 1 0 auto; 369 | } 370 | div#chat_div { 371 | grid-column: 3; 372 | } 373 | } 374 | 375 | div#footer p { 376 | margin-bottom: 0; 377 | margin-top: 0.5em; 378 | color: var(--dark4); 379 | } 380 | 381 | div#footer a.padded { 382 | margin-right: 20px; 383 | } 384 | 385 | a { 386 | color: var(--dark4); 387 | } 388 | 389 | /******************************************************************************/ 390 | 391 | svg.example { 392 | width: 75%; 393 | height: 250px; 394 | border:1px solid var(--dark4); 395 | background-color: var(--light0); 396 | } 397 | -------------------------------------------------------------------------------- /pont-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::convert::FromWasmAbi; 2 | use wasm_bindgen::prelude::*; 3 | use wasm_bindgen::JsCast; 4 | 5 | use std::collections::{HashMap, HashSet}; 6 | use std::sync::Mutex; 7 | use web_sys::{ 8 | AddEventListenerOptions, Blob, Document, Element, Event, EventTarget, 9 | FileReader, HtmlButtonElement, HtmlElement, HtmlInputElement, 10 | KeyboardEvent, MessageEvent, PointerEvent, ProgressEvent, 11 | SvgGraphicsElement, WebSocket, 12 | }; 13 | 14 | use pont_common::{ClientMessage, Color, Game, Piece, ServerMessage, Shape}; 15 | 16 | // Minimal logging macro 17 | macro_rules! console_log { 18 | ($($t:tt)*) => (web_sys::console::log_1(&format!($($t)*).into())) 19 | } 20 | 21 | type JsResult = Result; 22 | type JsError = Result<(), JsValue>; 23 | type JsClosure = Closure JsError>; 24 | 25 | trait DocExt { 26 | fn create_svg_element(&self, t: &str) -> JsResult; 27 | } 28 | 29 | impl DocExt for Document { 30 | fn create_svg_element(&self, t: &str) -> JsResult { 31 | self.create_element_ns(Some("http://www.w3.org/2000/svg"), t) 32 | } 33 | } 34 | 35 | fn get_time_ms() -> f64 { 36 | web_sys::window() 37 | .expect("No global window found") 38 | .performance() 39 | .expect("No performance object found") 40 | .now() 41 | } 42 | 43 | //////////////////////////////////////////////////////////////////////////////// 44 | 45 | macro_rules! methods { 46 | ($($sub:ident => [$($name:ident($($var:ident: $type:ty),*)),+ $(,)?]),+ 47 | $(,)?) => 48 | { 49 | $($( 50 | fn $name(&mut self, $($var: $type),* ) -> JsError { 51 | match self { 52 | State::$sub(s) => s.$name($($var),*), 53 | _ => panic!("Invalid state transition"), 54 | } 55 | } 56 | )+)+ 57 | } 58 | } 59 | 60 | macro_rules! transitions { 61 | ($($sub:ident => [$($name:ident($($var:ident: $type:ty),*) 62 | -> $into:ident),+ $(,)?]),+$(,)?) => 63 | { 64 | $($( 65 | fn $name(&mut self, $($var: $type),* ) -> JsError { 66 | let s = std::mem::replace(self, State::Empty); 67 | match s { 68 | State::$sub(s) => *self = State::$into(s.$name($($var),*)?), 69 | _ => panic!("Invalid state"), 70 | } 71 | Ok(()) 72 | } 73 | )+)+ 74 | } 75 | } 76 | 77 | //////////////////////////////////////////////////////////////////////////////// 78 | 79 | type Pos = (f32, f32); 80 | #[derive(PartialEq)] 81 | struct Dragging { 82 | target: Element, 83 | shadow: Element, 84 | offset: Pos, 85 | grid_origin: Option<(i32, i32)>, 86 | hand_index: usize, 87 | pointer_id: i32, 88 | } 89 | 90 | #[derive(PartialEq)] 91 | struct Panning { 92 | target: Element, 93 | pos: Pos, 94 | pointer_id: i32, 95 | } 96 | 97 | #[derive(PartialEq)] 98 | struct TileAnimation { 99 | target: Element, 100 | start: Pos, 101 | end: Pos, 102 | t0: f64, 103 | } 104 | 105 | impl TileAnimation { 106 | // Returns true if the animation should keep running 107 | fn run(&self, t: f64) -> JsResult { 108 | let anim_length = 100.0; 109 | let mut frac = ((t - self.t0) / anim_length) as f32; 110 | if frac > 1.0 { 111 | frac = 1.0; 112 | } 113 | let x = self.start.0 * (1.0 - frac) + self.end.0 * frac; 114 | let y = self.start.1 * (1.0 - frac) + self.end.1 * frac; 115 | self.target 116 | .set_attribute("transform", &format!("translate({x} {y})"))?; 117 | Ok(frac < 1.0) 118 | } 119 | } 120 | 121 | #[derive(PartialEq)] 122 | struct DropToGrid { 123 | anim: TileAnimation, 124 | shadow: Element, 125 | } 126 | 127 | #[derive(PartialEq)] 128 | struct ConsolidateHand(Vec); 129 | #[derive(PartialEq)] 130 | struct DropManyToGrid(Vec); 131 | #[derive(PartialEq)] 132 | struct HandSwap(TileAnimation, TileAnimation); 133 | #[derive(PartialEq)] 134 | struct ReturnAllToHand(Vec); 135 | #[derive(PartialEq)] 136 | struct ReturnToHand(TileAnimation); 137 | 138 | #[derive(PartialEq)] 139 | enum DragAnim { 140 | ConsolidateHand(ConsolidateHand), 141 | DropManyToGrid(DropManyToGrid), 142 | DropToGrid(DropToGrid), 143 | HandSwap(HandSwap), 144 | ReturnAllToHand(ReturnAllToHand), 145 | ReturnToHand(ReturnToHand), 146 | } 147 | 148 | #[derive(PartialEq)] 149 | enum BoardState { 150 | Idle, 151 | Dragging(Dragging), 152 | Panning(Panning), 153 | Animation(DragAnim), 154 | } 155 | 156 | enum DropTarget { 157 | DropToGrid(i32, i32), 158 | ReturnToGrid(i32, i32), 159 | Exchange, 160 | ReturnToHand(usize), 161 | } 162 | 163 | enum Move { 164 | Place(Vec<(Piece, i32, i32)>), 165 | Swap(Vec), 166 | } 167 | 168 | pub struct Board { 169 | doc: Document, 170 | svg: SvgGraphicsElement, 171 | svg_div: Element, 172 | 173 | state: BoardState, 174 | 175 | pan_group: Element, 176 | pan_offset: Pos, 177 | 178 | grid: HashMap<(i32, i32), Piece>, 179 | tentative: HashMap<(i32, i32), usize>, 180 | exchange_list: Vec, 181 | my_turn: bool, 182 | pieces_remaining: usize, 183 | hand: Vec<(Piece, Element)>, 184 | 185 | accept_button: HtmlButtonElement, 186 | reject_button: HtmlButtonElement, 187 | exchange_div: Element, 188 | count_div: Element, 189 | 190 | tentative_score_span: Option, 191 | 192 | pointer_down_cb: JsClosure, 193 | pointer_move_cb: JsClosure, 194 | pointer_up_cb: JsClosure, 195 | touch_start_cb: JsClosure, 196 | 197 | pan_move_cb: JsClosure, 198 | pan_end_cb: JsClosure, 199 | 200 | anim_cb: JsClosure, 201 | } 202 | 203 | impl Board { 204 | fn new(doc: &Document) -> JsResult { 205 | let pan_rect = doc 206 | .get_element_by_id("pan_rect") 207 | .expect("Could not find pan rect"); 208 | set_event_cb(&pan_rect, "pointerdown", move |evt: PointerEvent| { 209 | HANDLE.lock().unwrap().on_pan_start(evt) 210 | }) 211 | .forget(); 212 | 213 | let accept_button = doc 214 | .get_element_by_id("accept_button") 215 | .expect("Could not find accept_button") 216 | .dyn_into()?; 217 | set_event_cb(&accept_button, "click", move |evt: Event| { 218 | HANDLE.lock().unwrap().on_accept_button(evt) 219 | }) 220 | .forget(); 221 | 222 | let reject_button = doc 223 | .get_element_by_id("reject_button") 224 | .expect("Could not find reject_button") 225 | .dyn_into()?; 226 | set_event_cb(&reject_button, "click", move |evt: Event| { 227 | HANDLE.lock().unwrap().on_reject_button(evt) 228 | }) 229 | .forget(); 230 | 231 | let pointer_down_cb = build_cb(move |evt: PointerEvent| { 232 | HANDLE.lock().unwrap().on_pointer_down(evt) 233 | }); 234 | let pointer_move_cb = build_cb(move |evt: PointerEvent| { 235 | HANDLE.lock().unwrap().on_pointer_move(evt) 236 | }); 237 | let pointer_up_cb = build_cb(move |evt: PointerEvent| { 238 | HANDLE.lock().unwrap().on_pointer_up(evt) 239 | }); 240 | let anim_cb = 241 | build_cb(move |evt: f64| HANDLE.lock().unwrap().on_anim(evt)); 242 | let pan_move_cb = build_cb(move |evt: PointerEvent| { 243 | HANDLE.lock().unwrap().on_pan_move(evt) 244 | }); 245 | let pan_end_cb = 246 | build_cb(move |evt: Event| HANDLE.lock().unwrap().on_pan_end(evt)); 247 | let touch_start_cb = build_cb(move |evt: Event| { 248 | evt.prevent_default(); 249 | Ok(()) 250 | }); 251 | 252 | let svg = doc 253 | .get_element_by_id("game_svg") 254 | .expect("Could not find game svg") 255 | .dyn_into()?; 256 | let svg_div = doc 257 | .get_element_by_id("svg_div") 258 | .expect("Could not find svg div"); 259 | let pan_group = doc 260 | .get_element_by_id("pan_group") 261 | .expect("Could not find pan_group"); 262 | let exchange_div = doc 263 | .get_element_by_id("exchange_div") 264 | .expect("Could not find exchange_div"); 265 | let count_div = doc 266 | .get_element_by_id("count_div") 267 | .expect("Could not find count_div"); 268 | 269 | let out = Board { 270 | doc: doc.clone(), 271 | state: BoardState::Idle, 272 | svg, 273 | svg_div, 274 | pan_group, 275 | pan_offset: (0.0, 0.0), 276 | grid: HashMap::new(), 277 | tentative: HashMap::new(), 278 | my_turn: false, 279 | exchange_list: Vec::new(), 280 | hand: Vec::new(), 281 | pointer_down_cb, 282 | pointer_up_cb, 283 | pointer_move_cb, 284 | touch_start_cb, 285 | pan_move_cb, 286 | pan_end_cb, 287 | anim_cb, 288 | accept_button, 289 | reject_button, 290 | exchange_div, 291 | count_div, 292 | pieces_remaining: 0, 293 | tentative_score_span: None, 294 | }; 295 | 296 | Ok(out) 297 | } 298 | 299 | fn set_my_turn(&mut self, is_my_turn: bool) -> JsError { 300 | self.my_turn = is_my_turn; 301 | if is_my_turn { 302 | self.svg_div.class_list().remove_1("nyt")?; 303 | } else { 304 | self.svg_div.class_list().add_1("nyt")?; 305 | } 306 | self.update_exchange_div(is_my_turn) 307 | } 308 | 309 | fn get_transform(e: &Element) -> Pos { 310 | let t = e.get_attribute("transform").unwrap(); 311 | let s = t 312 | .chars() 313 | .filter(|&c| c.is_digit(10) || c == ' ' || c == '.' || c == '-') 314 | .collect::(); 315 | let mut itr = s.split(' ').map(|s| s.parse().unwrap()); 316 | 317 | let dx = itr.next().unwrap(); 318 | let dy = itr.next().unwrap(); 319 | 320 | (dx, dy) 321 | } 322 | 323 | fn mouse_pos(&self, evt: &PointerEvent) -> Pos { 324 | let mat = self.svg.get_screen_ctm().unwrap(); 325 | let x = (evt.client_x() as f32 - mat.e()) / mat.a(); 326 | let y = (evt.client_y() as f32 - mat.f()) / mat.d(); 327 | (x, y) 328 | } 329 | 330 | fn on_pan_start(&mut self, evt: PointerEvent) -> JsError { 331 | if self.state != BoardState::Idle { 332 | return Ok(()); 333 | } 334 | // No panning before placing the first piece, to prevent griefing by 335 | // placing the piece far from the visible region. 336 | if self.grid.is_empty() { 337 | return Ok(()); 338 | } 339 | 340 | evt.prevent_default(); 341 | let target = evt.target().unwrap().dyn_into::()?; 342 | target.set_pointer_capture(evt.pointer_id())?; 343 | 344 | let mut options = AddEventListenerOptions::new(); 345 | options.passive(false); 346 | let pointer_id = evt.pointer_id(); 347 | target.set_pointer_capture(pointer_id)?; 348 | target 349 | .add_event_listener_with_callback_and_add_event_listener_options( 350 | "pointermove", 351 | self.pan_move_cb.as_ref().unchecked_ref(), 352 | &options, 353 | )?; 354 | target 355 | .add_event_listener_with_callback_and_add_event_listener_options( 356 | "pointerup", 357 | self.pan_end_cb.as_ref().unchecked_ref(), 358 | &options, 359 | )?; 360 | self.doc 361 | .body() 362 | .expect("Could not get boby") 363 | .add_event_listener_with_callback_and_add_event_listener_options( 364 | "pointermove", 365 | self.pan_move_cb.as_ref().unchecked_ref(), 366 | &options, 367 | )?; 368 | 369 | let p = self.mouse_pos(&evt); 370 | self.state = BoardState::Panning(Panning { 371 | target, 372 | pointer_id, 373 | pos: (p.0 - self.pan_offset.0, p.1 - self.pan_offset.1), 374 | }); 375 | 376 | Ok(()) 377 | } 378 | 379 | fn on_pan_move(&mut self, evt: PointerEvent) -> JsError { 380 | if let BoardState::Panning(d) = &self.state { 381 | evt.prevent_default(); 382 | 383 | let p = self.mouse_pos(&evt); 384 | self.pan_offset = (p.0 - d.pos.0, p.1 - d.pos.1); 385 | self.pan_group.set_attribute( 386 | "transform", 387 | &format!( 388 | "translate({} {})", 389 | self.pan_offset.0, self.pan_offset.1 390 | ), 391 | ) 392 | } else { 393 | Err(JsValue::from_str("Invalid state (pan move)")) 394 | } 395 | } 396 | 397 | fn on_pan_end(&mut self, evt: Event) -> JsError { 398 | evt.prevent_default(); 399 | 400 | if let BoardState::Panning(d) = &self.state { 401 | d.target.release_pointer_capture(d.pointer_id)?; 402 | d.target.remove_event_listener_with_callback( 403 | "pointermove", 404 | self.pan_move_cb.as_ref().unchecked_ref(), 405 | )?; 406 | d.target.remove_event_listener_with_callback( 407 | "pointerup", 408 | self.pan_end_cb.as_ref().unchecked_ref(), 409 | )?; 410 | self.doc 411 | .body() 412 | .expect("Could not get boby") 413 | .remove_event_listener_with_callback( 414 | "pointermove", 415 | self.pan_move_cb.as_ref().unchecked_ref(), 416 | )?; 417 | self.state = BoardState::Idle; 418 | Ok(()) 419 | } else { 420 | Err(JsValue::from_str("Invalid state (pan end)")) 421 | } 422 | } 423 | 424 | fn on_pointer_down(&mut self, evt: PointerEvent) -> JsError { 425 | // We only drag if nothing else is dragging; 426 | // no fancy multi-touch dragging here. 427 | if self.state != BoardState::Idle { 428 | return Ok(()); 429 | } 430 | evt.prevent_default(); 431 | 432 | let mut target = evt.target().unwrap().dyn_into::()?; 433 | 434 | // Shadow goes underneath the dragged piece 435 | let shadow = self.doc.create_svg_element("rect")?; 436 | shadow.class_list().add_1("shadow")?; 437 | shadow.set_attribute("width", "9.5")?; 438 | shadow.set_attribute("height", "9.5")?; 439 | shadow.set_attribute("x", "0.25")?; 440 | shadow.set_attribute("y", "0.25")?; 441 | shadow.set_attribute("visibility", "hidden")?; 442 | self.pan_group.append_child(&shadow)?; 443 | 444 | // Walk up the tree to find the piece's group, 445 | // which sets its position with a translation 446 | while !target.has_attribute("transform") { 447 | target = target.parent_node().unwrap().dyn_into::()?; 448 | } 449 | let (mx, my) = self.mouse_pos(&evt); 450 | let (mut tx, mut ty) = Self::get_transform(&target); 451 | 452 | let (hand_index, grid_origin) = if my > 185.0 { 453 | // Picking from hand 454 | let i = (tx.round() as i32 - 5) / 15; 455 | self.svg.remove_child(&target)?; 456 | (i as usize, None) 457 | } else { 458 | // Picking from tentative grid 459 | let x = tx.round() as i32 / 10; 460 | let y = ty.round() as i32 / 10; 461 | self.pan_group.remove_child(&target)?; 462 | tx += self.pan_offset.0; 463 | ty += self.pan_offset.1; 464 | target.set_attribute( 465 | "transform", 466 | &format!("translate({} {})", tx, ty), 467 | )?; 468 | let hand_index = self.tentative.remove(&(x, y)).unwrap(); 469 | if self.tentative.is_empty() { 470 | self.accept_button.set_disabled(true); 471 | self.reject_button.set_disabled(true); 472 | } 473 | self.update_exchange_div(true)?; 474 | let valid = self.mark_invalid()?; 475 | self.set_estimated_score(valid); 476 | (hand_index, Some((x, y))) 477 | }; 478 | target.class_list().remove_1("invalid")?; 479 | 480 | // Move to the back of the SVG object, so it's on top 481 | self.svg.append_child(&target)?; 482 | 483 | let mut options = AddEventListenerOptions::new(); 484 | options.passive(false); 485 | let pointer_id = evt.pointer_id(); 486 | target.set_pointer_capture(pointer_id)?; 487 | target 488 | .add_event_listener_with_callback_and_add_event_listener_options( 489 | "pointermove", 490 | self.pointer_move_cb.as_ref().unchecked_ref(), 491 | &options, 492 | )?; 493 | target 494 | .add_event_listener_with_callback_and_add_event_listener_options( 495 | "pointerup", 496 | self.pointer_up_cb.as_ref().unchecked_ref(), 497 | &options, 498 | )?; 499 | self.doc 500 | .body() 501 | .expect("Could not get boby") 502 | .add_event_listener_with_callback_and_add_event_listener_options( 503 | "pointermove", 504 | self.pointer_move_cb.as_ref().unchecked_ref(), 505 | &options, 506 | )?; 507 | 508 | self.state = BoardState::Dragging(Dragging { 509 | target, 510 | shadow, 511 | offset: (mx - tx, my - ty), 512 | hand_index, 513 | grid_origin, 514 | pointer_id, 515 | }); 516 | Ok(()) 517 | } 518 | 519 | fn drop_target(&self, evt: &PointerEvent) -> JsResult<(Pos, DropTarget)> { 520 | if let BoardState::Dragging(d) = &self.state { 521 | // Get the position of the tile being dragged 522 | // in SVG frame coordinates (0-200) 523 | let (mut x, mut y) = self.mouse_pos(evt); 524 | x -= d.offset.0; 525 | y -= d.offset.1; 526 | 527 | // Clamp to the window's bounds 528 | for c in [&mut x, &mut y].iter_mut() { 529 | if **c < 0.0 { 530 | **c = 0.0; 531 | } else if **c > 190.0 { 532 | **c = 190.0; 533 | } 534 | } 535 | // If this isn't your turn, then you're only allowed to rearrange 536 | // pieces within your rack, which we enforce by clamping x and y 537 | if !self.my_turn { 538 | if y < 175.0 { 539 | y = 175.0; 540 | } 541 | if x > 87.0 { 542 | x = 87.0; 543 | } 544 | } 545 | 546 | // If we've started exchanging tiles, then prevent folks from 547 | // dragging onto the grid. 548 | if !self.exchange_list.is_empty() && y < 175.0 { 549 | y = 175.0; 550 | } 551 | let pos = (x, y); 552 | 553 | // If the tile is off the bottom of the grid, then we propose 554 | // to return it to the hand. 555 | if y >= 165.0 { 556 | if self.tentative.is_empty() 557 | && x >= 95.0 558 | && x <= 140.0 559 | && self.exchange_list.len() < self.pieces_remaining 560 | { 561 | return Ok((pos, DropTarget::Exchange)); 562 | } else { 563 | return Ok(( 564 | pos, 565 | DropTarget::ReturnToHand(((x + 2.5) / 15.0) as usize), 566 | )); 567 | } 568 | } 569 | 570 | // Otherwise, we shift the tile's coordinates by the panning 571 | // of the main grid, then check whether we can place it 572 | x -= self.pan_offset.0; 573 | y -= self.pan_offset.1; 574 | 575 | let tx = (x / 10.0).round() as i32; 576 | let ty = (y / 10.0).round() as i32; 577 | 578 | let offboard = { 579 | let x = tx as f32 * 10.0 + self.pan_offset.0; 580 | let y = ty as f32 * 10.0 + self.pan_offset.1; 581 | x < 0.0 || y < 0.0 || y > 165.0 || x >= 190.0 582 | }; 583 | 584 | let overlapping = self.grid.contains_key(&(tx, ty)) 585 | || self.tentative.contains_key(&(tx, ty)); 586 | if !overlapping && !offboard { 587 | return Ok((pos, DropTarget::DropToGrid(tx, ty))); 588 | } 589 | 590 | // Otherwise, return to either the hand or the grid 591 | Ok(( 592 | pos, 593 | match d.grid_origin { 594 | None => DropTarget::ReturnToHand(d.hand_index), 595 | Some((gx, gy)) => DropTarget::ReturnToGrid(gx, gy), 596 | }, 597 | )) 598 | } else { 599 | Err(JsValue::from_str("Invalid state (drop target)")) 600 | } 601 | } 602 | 603 | fn on_pointer_move(&self, evt: PointerEvent) -> JsError { 604 | if let BoardState::Dragging(d) = &self.state { 605 | evt.prevent_default(); 606 | 607 | let (pos, drop_target) = self.drop_target(&evt)?; 608 | d.target.set_attribute( 609 | "transform", 610 | &format!("translate({} {})", pos.0, pos.1), 611 | )?; 612 | if let DropTarget::DropToGrid(gx, gy) = drop_target { 613 | d.shadow.set_attribute( 614 | "transform", 615 | &format!( 616 | "translate({} {})", 617 | gx as f32 * 10.0, 618 | gy as f32 * 10.0 619 | ), 620 | )?; 621 | d.shadow.set_attribute("visibility", "visible") 622 | } else { 623 | d.shadow.set_attribute("visibility", "hidden") 624 | } 625 | } else { 626 | Err(JsValue::from_str("Invalid state (pointer move)")) 627 | } 628 | } 629 | 630 | fn get_score(&self) -> Option { 631 | let mut g = Game { 632 | board: self.grid.clone(), 633 | bag: Vec::new(), 634 | }; 635 | let ps = self 636 | .tentative 637 | .iter() 638 | .map(|(pos, index)| (self.hand[*index].0, pos.0, pos.1)) 639 | .collect::>(); 640 | g.play(&ps) 641 | } 642 | 643 | fn set_estimated_score(&self, valid: bool) { 644 | if let Some(score) = self.get_score().filter(|s| *s > 0 && valid) { 645 | self.tentative_score_span 646 | .as_ref() 647 | .unwrap() 648 | .set_text_content(Some(&format!(" [+{}]", score))); 649 | } else { 650 | self.tentative_score_span 651 | .as_ref() 652 | .unwrap() 653 | .set_text_content(Some("")); 654 | } 655 | } 656 | 657 | fn mark_invalid(&self) -> JsResult { 658 | let mut b = self.grid.clone(); 659 | for (pos, index) in self.tentative.iter() { 660 | b.insert(*pos, self.hand[*index].0); 661 | } 662 | let mut invalid = Game::invalid(&b); 663 | let play = self.tentative.keys().cloned().collect::>(); 664 | if !Game::is_linear_connected(&b, &play) { 665 | for pos in self.tentative.keys() { 666 | invalid.insert(*pos); 667 | } 668 | } 669 | for (pos, index) in self.tentative.iter() { 670 | if invalid.contains(pos) { 671 | self.hand[*index].1.class_list().add_1("invalid")?; 672 | } else { 673 | self.hand[*index].1.class_list().remove_1("invalid")?; 674 | } 675 | } 676 | Ok(invalid.is_empty()) 677 | } 678 | 679 | fn release_drag_captures(&self, d: &Dragging) -> JsError { 680 | d.target.release_pointer_capture(d.pointer_id)?; 681 | d.target.remove_event_listener_with_callback( 682 | "pointermove", 683 | self.pointer_move_cb.as_ref().unchecked_ref(), 684 | )?; 685 | d.target.remove_event_listener_with_callback( 686 | "pointerup", 687 | self.pointer_up_cb.as_ref().unchecked_ref(), 688 | )?; 689 | self.doc 690 | .body() 691 | .expect("Could not get boby") 692 | .remove_event_listener_with_callback( 693 | "pointermove", 694 | self.pointer_move_cb.as_ref().unchecked_ref(), 695 | )?; 696 | Ok(()) 697 | } 698 | 699 | fn on_pointer_up(&mut self, evt: PointerEvent) -> JsError { 700 | if let BoardState::Dragging(d) = &self.state { 701 | evt.prevent_default(); 702 | self.release_drag_captures(d)?; 703 | 704 | let (pos, drop_target) = self.drop_target(&evt)?; 705 | let drag_anim = match drop_target { 706 | DropTarget::ReturnToHand(i) => { 707 | self.pan_group.remove_child(&d.shadow)?; 708 | if i == d.hand_index || i >= self.hand.len() { 709 | Some(DragAnim::ReturnToHand(ReturnToHand( 710 | TileAnimation { 711 | target: d.target.clone(), 712 | start: pos, 713 | end: ((d.hand_index * 15 + 5) as f32, 185.0), 714 | t0: evt.time_stamp(), 715 | }, 716 | ))) 717 | } else { 718 | // Check to see if the target is staged in the grid 719 | // or exchange region, in which case, we do a slightly 720 | // modified animation 721 | let mut target_in_hand = true; 722 | for (_k, v) in self.tentative.iter_mut() { 723 | if *v == i { 724 | *v = d.hand_index; 725 | target_in_hand = false; 726 | } 727 | } 728 | for v in self.exchange_list.iter_mut() { 729 | if *v == i { 730 | *v = d.hand_index; 731 | target_in_hand = false; 732 | } 733 | } 734 | self.hand.swap(i, d.hand_index); 735 | 736 | // If the target is in the hand, then animate both 737 | // swapping places; otherwise, just animate the returned 738 | // piece (leaving the other piece on the board) 739 | if target_in_hand { 740 | Some(DragAnim::HandSwap(HandSwap( 741 | TileAnimation { 742 | target: d.target.clone(), 743 | start: pos, 744 | end: ((i * 15 + 5) as f32, 185.0), 745 | t0: evt.time_stamp(), 746 | }, 747 | TileAnimation { 748 | target: self.hand[d.hand_index].1.clone(), 749 | start: ((i * 15 + 5) as f32, 185.0), 750 | end: ( 751 | (d.hand_index * 15 + 5) as f32, 752 | 185.0, 753 | ), 754 | t0: evt.time_stamp(), 755 | }, 756 | ))) 757 | } else { 758 | Some(DragAnim::ReturnToHand(ReturnToHand( 759 | TileAnimation { 760 | target: d.target.clone(), 761 | start: pos, 762 | end: ((i * 15 + 5) as f32, 185.0), 763 | t0: evt.time_stamp(), 764 | }, 765 | ))) 766 | } 767 | } 768 | } 769 | DropTarget::DropToGrid(gx, gy) 770 | | DropTarget::ReturnToGrid(gx, gy) => { 771 | self.tentative.insert((gx, gy), d.hand_index); 772 | let target = d.target.clone(); 773 | self.svg.remove_child(&target)?; 774 | self.pan_group.append_child(&target)?; 775 | Some(DragAnim::DropToGrid(DropToGrid { 776 | anim: TileAnimation { 777 | target, 778 | start: ( 779 | pos.0 - self.pan_offset.0, 780 | pos.1 - self.pan_offset.1, 781 | ), 782 | end: (gx as f32 * 10.0, gy as f32 * 10.0), 783 | t0: evt.time_stamp(), 784 | }, 785 | shadow: d.shadow.clone(), 786 | })) 787 | } 788 | DropTarget::Exchange => { 789 | self.pan_group.remove_child(&d.shadow)?; 790 | self.exchange_list.push(d.hand_index); 791 | d.target.set_attribute("visibility", "hidden")?; 792 | self.update_exchange_div(true)?; 793 | self.accept_button.set_disabled(false); 794 | self.reject_button.set_disabled(false); 795 | 796 | // No animation here, because we wait for the server to 797 | // send back a MoveAccepted message then consolidate hand 798 | None 799 | } 800 | }; 801 | let valid = self.mark_invalid()?; 802 | self.set_estimated_score(valid); 803 | self.update_exchange_div(true)?; 804 | if let Some(drag) = drag_anim { 805 | self.state = BoardState::Animation(drag); 806 | self.request_animation_frame()?; 807 | } else { 808 | self.state = BoardState::Idle; 809 | } 810 | } 811 | Ok(()) 812 | } 813 | 814 | fn on_anim(&mut self, t: f64) -> JsError { 815 | if let BoardState::Animation(drag) = &mut self.state { 816 | match drag { 817 | DragAnim::DropToGrid(d) => { 818 | if d.anim.run(t)? { 819 | self.request_animation_frame()?; 820 | } else { 821 | self.pan_group.remove_child(&d.shadow)?; 822 | self.state = BoardState::Idle; 823 | let valid = self.mark_invalid()?; 824 | self.accept_button.set_disabled(!valid); 825 | self.reject_button.set_disabled(false); 826 | self.set_estimated_score(valid); 827 | } 828 | } 829 | DragAnim::ReturnToHand(d) => { 830 | if d.0.run(t)? { 831 | self.request_animation_frame()?; 832 | } else { 833 | self.state = BoardState::Idle; 834 | let mut valid = true; 835 | if !self.tentative.is_empty() { 836 | valid = self.mark_invalid()?; 837 | self.accept_button.set_disabled(!valid); 838 | } else if self.exchange_list.is_empty() { 839 | self.accept_button.set_disabled(true); 840 | self.reject_button.set_disabled(true); 841 | } 842 | self.set_estimated_score(valid); 843 | } 844 | } 845 | DragAnim::HandSwap(HandSwap(a, b)) => { 846 | if a.run(t)? | b.run(t)? { 847 | // non short-circuiting or! 848 | self.request_animation_frame()?; 849 | } else { 850 | self.state = BoardState::Idle; 851 | if !self.tentative.is_empty() { 852 | self.accept_button 853 | .set_disabled(!self.mark_invalid()?); 854 | } else if self.exchange_list.is_empty() { 855 | self.accept_button.set_disabled(true); 856 | self.reject_button.set_disabled(true); 857 | } 858 | } 859 | } 860 | DragAnim::ReturnAllToHand(d) => { 861 | let mut any_running = false; 862 | for a in d.0.iter() { 863 | any_running |= a.run(t)?; 864 | } 865 | if any_running { 866 | self.request_animation_frame()?; 867 | } else { 868 | self.state = BoardState::Idle; 869 | self.accept_button.set_disabled(true); 870 | self.reject_button.set_disabled(true); 871 | self.update_exchange_div(true)?; 872 | self.set_estimated_score(false); 873 | } 874 | } 875 | DragAnim::ConsolidateHand(ConsolidateHand(d)) 876 | | DragAnim::DropManyToGrid(DropManyToGrid(d)) => { 877 | let mut any_running = false; 878 | for a in d.iter() { 879 | any_running |= a.run(t)?; 880 | } 881 | if any_running { 882 | self.request_animation_frame()?; 883 | } else { 884 | self.state = BoardState::Idle; 885 | } 886 | } 887 | } 888 | } 889 | Ok(()) 890 | } 891 | 892 | fn request_animation_frame(&self) -> JsResult { 893 | web_sys::window() 894 | .expect("no global `window` exists") 895 | .request_animation_frame(self.anim_cb.as_ref().unchecked_ref()) 896 | } 897 | 898 | fn add_hand(&mut self, p: Piece) -> JsResult { 899 | let g = self.new_piece(p)?; 900 | self.svg.append_child(&g)?; 901 | g.class_list().add_1("piece")?; 902 | g.set_attribute( 903 | "transform", 904 | &format!("translate({} 185)", 5 + 15 * self.hand.len()), 905 | )?; 906 | 907 | let mut options = AddEventListenerOptions::new(); 908 | options.passive(false); 909 | g.add_event_listener_with_callback_and_add_event_listener_options( 910 | "pointerdown", 911 | self.pointer_down_cb.as_ref().unchecked_ref(), 912 | &options, 913 | )?; 914 | g.add_event_listener_with_callback_and_add_event_listener_options( 915 | "touchstart", 916 | self.touch_start_cb.as_ref().unchecked_ref(), 917 | &options, 918 | )?; 919 | 920 | self.hand.push((p, g.clone())); 921 | 922 | Ok(g) 923 | } 924 | 925 | fn new_piece(&self, p: Piece) -> JsResult { 926 | let g = self.doc.create_svg_element("g")?; 927 | let r = self.doc.create_svg_element("rect")?; 928 | r.class_list().add_1("tile")?; 929 | r.set_attribute("width", "9.5")?; 930 | r.set_attribute("height", "9.5")?; 931 | r.set_attribute("x", "0.25")?; 932 | r.set_attribute("y", "0.25")?; 933 | let s = match p.0 { 934 | Shape::Circle => { 935 | let s = self.doc.create_svg_element("circle")?; 936 | s.set_attribute("r", "3.0")?; 937 | s.set_attribute("cx", "5.0")?; 938 | s.set_attribute("cy", "5.0")?; 939 | s 940 | } 941 | Shape::Square => { 942 | let s = self.doc.create_svg_element("rect")?; 943 | s.set_attribute("width", "6.0")?; 944 | s.set_attribute("height", "6.0")?; 945 | s.set_attribute("x", "2.0")?; 946 | s.set_attribute("y", "2.0")?; 947 | s 948 | } 949 | Shape::Clover => { 950 | let s = self.doc.create_svg_element("g")?; 951 | for (x, y) in &[(5.0, 3.0), (5.0, 7.0), (3.0, 5.0), (7.0, 5.0)] 952 | { 953 | let c = self.doc.create_svg_element("circle")?; 954 | c.set_attribute("r", "1.5")?; 955 | c.set_attribute("cx", &x.to_string())?; 956 | c.set_attribute("cy", &y.to_string())?; 957 | s.append_child(&c)?; 958 | } 959 | let r = self.doc.create_svg_element("rect")?; 960 | r.set_attribute("width", "4.0")?; 961 | r.set_attribute("height", "3.0")?; 962 | r.set_attribute("x", "3.0")?; 963 | r.set_attribute("y", "3.5")?; 964 | s.append_child(&r)?; 965 | 966 | let r = self.doc.create_svg_element("rect")?; 967 | r.set_attribute("width", "3.0")?; 968 | r.set_attribute("height", "4.0")?; 969 | r.set_attribute("x", "3.5")?; 970 | r.set_attribute("y", "3.0")?; 971 | s.append_child(&r)?; 972 | 973 | s 974 | } 975 | Shape::Diamond => { 976 | let s = self.doc.create_svg_element("polygon")?; 977 | s.set_attribute("points", "2,5 5,8 8,5 5,2")?; 978 | s 979 | } 980 | Shape::Cross => { 981 | let s = self.doc.create_svg_element("polygon")?; 982 | s.set_attribute( 983 | "points", 984 | "2,2 3.5,5 2,8 5,6.5 8,8 6.5,5 8,2 5,3.5", 985 | )?; 986 | s 987 | } 988 | Shape::Star => { 989 | let g = self.doc.create_svg_element("g")?; 990 | let s = self.doc.create_svg_element("polygon")?; 991 | s.set_attribute("points", "3,3 4,5 3,7 5,6 7,7 6,5 7,3 5,4")?; 992 | g.append_child(&s)?; 993 | let s = self.doc.create_svg_element("polygon")?; 994 | s.set_attribute("points", "1,5 4,6 5,9 6,6 9,5 6,4 5,1 4,4")?; 995 | g.append_child(&s)?; 996 | g 997 | } 998 | }; 999 | s.class_list().add_1("color")?; 1000 | 1001 | g.append_child(&r)?; 1002 | g.append_child(&s)?; 1003 | g.class_list().add_1(match p.1 { 1004 | Color::Orange => "shape-orange", 1005 | Color::Yellow => "shape-yellow", 1006 | Color::Green => "shape-green", 1007 | Color::Red => "shape-red", 1008 | Color::Blue => "shape-blue", 1009 | Color::Purple => "shape-purple", 1010 | })?; 1011 | 1012 | // Add carets on the corners based on color, to be accessible 1013 | let mut pts = Vec::new(); 1014 | if p.1 == Color::Orange || p.1 == Color::Yellow { 1015 | pts.push("0.5,0.5 3,0.5 0.5,3"); 1016 | } 1017 | if p.1 == Color::Orange || p.1 == Color::Green { 1018 | pts.push("9.5,9.5 7,9.5 9.5,7"); 1019 | } 1020 | if p.1 == Color::Red || p.1 == Color::Blue { 1021 | pts.push("0.5,9.5 3,9.5 0.5,7"); 1022 | } 1023 | if p.1 == Color::Red || p.1 == Color::Purple { 1024 | pts.push("9.5,0.5 7,0.5 9.5,3"); 1025 | } 1026 | 1027 | for poly in pts.into_iter() { 1028 | let corner = self.doc.create_svg_element("polygon")?; 1029 | corner.set_attribute("points", poly)?; 1030 | corner.class_list().add_1("corner")?; 1031 | corner.class_list().add_1("color")?; 1032 | g.append_child(&corner)?; 1033 | } 1034 | 1035 | Ok(g) 1036 | } 1037 | 1038 | fn add_piece(&mut self, p: Piece, x: i32, y: i32) -> JsResult { 1039 | self.grid.insert((x, y), p); 1040 | 1041 | let g = self.new_piece(p)?; 1042 | self.pan_group.append_child(&g)?; 1043 | g.class_list().add_1("placed")?; 1044 | g.class_list().add_1("played")?; 1045 | g.set_attribute( 1046 | "transform", 1047 | &format!("translate({} {})", x * 10, y * 10), 1048 | )?; 1049 | 1050 | Ok(g) 1051 | } 1052 | 1053 | fn reset_played(&mut self) -> JsError { 1054 | let prev_played = self.doc.get_elements_by_class_name("played"); 1055 | // We empty out the collection by removing the 'played' class 1056 | while prev_played.length() > 0 { 1057 | prev_played 1058 | .item(0) 1059 | .ok_or_else(|| JsValue::from_str("Could not get item"))? 1060 | .class_list() 1061 | .remove_1("played")?; 1062 | } 1063 | Ok(()) 1064 | } 1065 | 1066 | fn on_reject_button(&mut self, evt: Event) -> JsError { 1067 | // Don't allow for any tricky business here 1068 | if self.state != BoardState::Idle { 1069 | return Ok(()); 1070 | } 1071 | 1072 | let drag = if !self.tentative.is_empty() { 1073 | let mut tiles = HashMap::new(); 1074 | std::mem::swap(&mut self.tentative, &mut tiles); 1075 | // Take every active tile and free them from the tile grid, 1076 | // adjusting their transform so they don't move at all 1077 | for i in tiles.values() { 1078 | let t = &self.hand[*i].1; 1079 | self.pan_group.remove_child(t)?; 1080 | t.class_list().remove_1("invalid")?; 1081 | let (dx, dy) = Self::get_transform(t); 1082 | t.set_attribute( 1083 | "transform", 1084 | &format!( 1085 | "translate({} {})", 1086 | dx - self.pan_offset.0, 1087 | dy - self.pan_offset.1 1088 | ), 1089 | )?; 1090 | self.svg.append_child(t)?; 1091 | } 1092 | Some(DragAnim::ReturnAllToHand(ReturnAllToHand( 1093 | tiles 1094 | .drain() 1095 | .map(|((tx, ty), i)| TileAnimation { 1096 | target: self.hand[i].1.clone(), 1097 | start: ( 1098 | tx as f32 * 10.0 + self.pan_offset.0, 1099 | ty as f32 * 10.0 + self.pan_offset.1, 1100 | ), 1101 | end: ((i * 15 + 5) as f32, 185.0), 1102 | t0: evt.time_stamp(), 1103 | }) 1104 | .collect(), 1105 | ))) 1106 | } else if !self.exchange_list.is_empty() { 1107 | let mut ex = Vec::new(); 1108 | std::mem::swap(&mut ex, &mut self.exchange_list); 1109 | self.update_exchange_div(true)?; 1110 | Some(DragAnim::ReturnAllToHand(ReturnAllToHand( 1111 | ex.drain(0..) 1112 | .map(|i| { 1113 | let target = self.hand[i].1.clone(); 1114 | target.set_attribute("visibility", "visible")?; 1115 | let x = (i * 15 + 5) as f32; 1116 | Ok(TileAnimation { 1117 | target, 1118 | start: (x, 200.0), 1119 | end: (x, 185.0), 1120 | t0: evt.time_stamp(), 1121 | }) 1122 | }) 1123 | .collect::>>()?, 1124 | ))) 1125 | } else { 1126 | None 1127 | }; 1128 | 1129 | if let Some(drag) = drag { 1130 | self.state = BoardState::Animation(drag); 1131 | self.request_animation_frame()?; 1132 | } 1133 | Ok(()) 1134 | } 1135 | 1136 | /* Attempts to make the given move. 1137 | * If the move is valid, returns the indexes of placed pieces 1138 | * (as hand indexes), which can be passed up to the server. */ 1139 | fn make_move(&mut self, _evt: Event) -> JsResult { 1140 | if self.state != BoardState::Idle { 1141 | return Ok(Move::Place(Vec::new())); 1142 | } 1143 | 1144 | // Disable everything until we hear back from the server. 1145 | // 1146 | // If this is a one-player game, then it will be our turn again, 1147 | // but we'll let the server tell us that. 1148 | self.accept_button.set_disabled(true); 1149 | self.reject_button.set_disabled(true); 1150 | 1151 | self.set_my_turn(false)?; 1152 | 1153 | if !self.tentative.is_empty() { 1154 | Ok(Move::Place( 1155 | self.tentative 1156 | .iter() 1157 | .map(|((x, y), i)| (self.hand[*i].0, *x, *y)) 1158 | .collect(), 1159 | )) 1160 | } else { 1161 | assert!(!self.exchange_list.is_empty()); 1162 | Ok(Move::Swap( 1163 | self.exchange_list.iter().map(|i| self.hand[*i].0).collect(), 1164 | )) 1165 | } 1166 | } 1167 | 1168 | fn on_move_accepted(&mut self, dealt: &[Piece]) -> JsError { 1169 | self.reset_played()?; 1170 | let mut placed = HashMap::new(); 1171 | for ((x, y), i) in self.tentative.drain() { 1172 | placed.insert(i, (x, y)); 1173 | } 1174 | let mut exchanged = HashSet::new(); 1175 | for i in self.exchange_list.drain(0..) { 1176 | exchanged.insert(i); 1177 | } 1178 | self.set_estimated_score(false); 1179 | 1180 | // We're going to shuffle pieces around now! 1181 | let mut prev_hand = Vec::new(); 1182 | std::mem::swap(&mut prev_hand, &mut self.hand); 1183 | let mut anims = Vec::new(); 1184 | let t0 = get_time_ms(); 1185 | for (i, (piece, element)) in prev_hand.into_iter().enumerate() { 1186 | if let Some((x, y)) = placed.remove(&i) { 1187 | element.class_list().remove_1("piece")?; 1188 | element.class_list().add_1("placed")?; 1189 | element.remove_event_listener_with_callback( 1190 | "pointerdown", 1191 | self.pointer_down_cb.as_ref().unchecked_ref(), 1192 | )?; 1193 | self.grid.insert((x, y), piece); 1194 | } else if exchanged.contains(&i) { 1195 | self.svg.remove_child(&element)?; 1196 | } else { 1197 | if self.hand.len() != i { 1198 | anims.push(TileAnimation { 1199 | target: element.clone(), 1200 | start: (i as f32 * 15.0 + 5.0, 185.0), 1201 | end: (self.hand.len() as f32 * 15.0 + 5.0, 185.0), 1202 | t0, 1203 | }); 1204 | } 1205 | self.hand.push((piece, element)); 1206 | } 1207 | } 1208 | for d in dealt { 1209 | let x = self.hand.len() as f32 * 15.0 + 5.0; 1210 | let target = self.add_hand(*d)?; 1211 | anims.push(TileAnimation { 1212 | target, 1213 | start: (x, 220.0), 1214 | end: (x, 185.0), 1215 | t0, 1216 | }) 1217 | } 1218 | self.state = BoardState::Animation(DragAnim::ConsolidateHand( 1219 | ConsolidateHand(anims), 1220 | )); 1221 | self.request_animation_frame()?; 1222 | 1223 | Ok(()) 1224 | } 1225 | 1226 | fn update_exchange_div(&mut self, my_turn: bool) -> JsError { 1227 | // Special case: if a new user joins while we've got pieces staged 1228 | // to swap, then it's possible that we won't have enough to swap, 1229 | // so we cancel the swap. 1230 | if self.pieces_remaining < self.exchange_list.len() { 1231 | // This will call update_exchange_div again, so we don't 1232 | // need to run any of the code below. 1233 | // 1234 | // TODO: this will fail if the player is panning or holding a piece 1235 | return self.on_reject_button(Event::new("dummy")?); 1236 | } 1237 | 1238 | // If there are no pieces remaining, then the box is always disabled 1239 | if self.pieces_remaining == 0 { 1240 | self.exchange_div 1241 | .set_inner_html("

No pieces
left in bag

"); 1242 | self.exchange_div.class_list().add_1("disabled")?; 1243 | return Ok(()); 1244 | } 1245 | 1246 | // If it's not your turn, then we disable the box but leave the normal 1247 | // text (because we know that there are pieces remaining). 1248 | if !my_turn { 1249 | self.exchange_div 1250 | .set_inner_html("

Drag here
to swap

"); 1251 | self.exchange_div.class_list().add_1("disabled")?; 1252 | return Ok(()); 1253 | } 1254 | 1255 | // If it's our turn and there are pieces staged in the grid, then we 1256 | // disable the swapping box. 1257 | if !self.tentative.is_empty() { 1258 | self.exchange_div 1259 | .set_inner_html("

Drag here
to swap

"); 1260 | self.exchange_div.class_list().add_1("disabled")?; 1261 | return Ok(()); 1262 | } 1263 | 1264 | // Otherwise, the box is enabled and we have appropriate text 1265 | self.exchange_div.class_list().remove_1("disabled")?; 1266 | let n = self.exchange_list.len(); 1267 | if n == 0 { 1268 | self.exchange_div 1269 | .set_inner_html("

Drag here
to swap

"); 1270 | } else { 1271 | self.exchange_div.set_inner_html(&format!( 1272 | "

Swap {} piece{}{}

", 1273 | n, 1274 | if n > 1 { "s" } else { " " }, 1275 | if n == self.pieces_remaining { 1276 | " (max)" 1277 | } else { 1278 | "" 1279 | } 1280 | )); 1281 | } 1282 | Ok(()) 1283 | } 1284 | 1285 | fn update_count_div(&mut self) -> JsError { 1286 | self.count_div.set_inner_html(&format!( 1287 | "

{} piece{} left in the bag

", 1288 | self.pieces_remaining, 1289 | if self.pieces_remaining == 1 { "" } else { "s" } 1290 | )); 1291 | Ok(()) 1292 | } 1293 | } 1294 | 1295 | //////////////////////////////////////////////////////////////////////////////// 1296 | 1297 | pub struct Base { 1298 | doc: Document, 1299 | ws: WebSocket, 1300 | } 1301 | 1302 | impl Base { 1303 | fn send(&self, msg: ClientMessage) -> JsError { 1304 | let encoded = bincode::serialize(&msg).map_err(|e| { 1305 | JsValue::from_str(&format!("Could not encode: {}", e)) 1306 | })?; 1307 | self.ws.send_with_u8_array(&encoded[..]) 1308 | } 1309 | } 1310 | 1311 | //////////////////////////////////////////////////////////////////////////////// 1312 | 1313 | // These are the states in the system 1314 | struct Connecting { 1315 | base: Base, 1316 | } 1317 | 1318 | struct CreateOrJoin { 1319 | base: Base, 1320 | 1321 | name_input: HtmlInputElement, 1322 | room_input: HtmlInputElement, 1323 | play_button: HtmlButtonElement, 1324 | colorblind_checkbox: HtmlInputElement, 1325 | err_div: HtmlElement, 1326 | err_span: HtmlElement, 1327 | 1328 | // Callbacks are owned so that it lives as long as the state 1329 | _room_invalid_cb: JsClosure, 1330 | _input_cb: JsClosure, 1331 | _submit_cb: JsClosure, 1332 | } 1333 | 1334 | struct Playing { 1335 | base: Base, 1336 | 1337 | chat_div: HtmlElement, 1338 | chat_input: HtmlInputElement, 1339 | score_table: HtmlElement, 1340 | player_index: usize, 1341 | active_player: usize, 1342 | player_names: Vec, 1343 | 1344 | board: Board, 1345 | 1346 | // Callback is owned so that it lives as long as the state 1347 | _keyup_cb: JsClosure, 1348 | } 1349 | 1350 | //////////////////////////////////////////////////////////////////////////////// 1351 | 1352 | enum State { 1353 | Connecting(Connecting), 1354 | CreateOrJoin(CreateOrJoin), 1355 | Playing(Playing), 1356 | Empty, 1357 | } 1358 | 1359 | impl State { 1360 | transitions!( 1361 | Connecting => [ 1362 | on_connected() -> CreateOrJoin, 1363 | ], 1364 | CreateOrJoin => [ 1365 | on_joined_room(room_name: &str, players: &[(String, u32, bool)], 1366 | active_player: usize, 1367 | player_index: usize, 1368 | board: &[((i32, i32), Piece)], 1369 | pieces: &[Piece]) -> Playing, 1370 | ], 1371 | ); 1372 | 1373 | methods!( 1374 | Playing => [ 1375 | on_pointer_down(evt: PointerEvent), 1376 | on_pointer_up(evt: PointerEvent), 1377 | on_pointer_move(evt: PointerEvent), 1378 | on_pan_start(evt: PointerEvent), 1379 | on_pan_move(evt: PointerEvent), 1380 | on_pan_end(evt: Event), 1381 | on_accept_button(evt: Event), 1382 | on_reject_button(evt: Event), 1383 | on_anim(t: f64), 1384 | on_send_chat(), 1385 | on_chat(from: &str, msg: &str), 1386 | on_information(msg: &str), 1387 | on_new_player(name: &str), 1388 | on_player_disconnected(index: usize), 1389 | on_player_reconnected(index: usize), 1390 | on_player_turn(active_player: usize), 1391 | on_played(pieces: &[(Piece, i32, i32)]), 1392 | on_swapped(count: usize), 1393 | on_move_accepted(dealt: &[Piece]), 1394 | on_move_rejected(), 1395 | on_pieces_remaining(remaining: usize), 1396 | on_player_score(delta: u32, total: u32), 1397 | on_finished(winner: usize), 1398 | ], 1399 | CreateOrJoin => [ 1400 | on_room_name_invalid(), 1401 | on_join_inputs_changed(), 1402 | on_join_button(), 1403 | on_join_failed(room: &str), 1404 | ], 1405 | ); 1406 | } 1407 | 1408 | unsafe impl Send for State { 1409 | /* YOLO */ 1410 | } 1411 | 1412 | lazy_static::lazy_static! { 1413 | static ref HANDLE: Mutex = Mutex::new(State::Empty); 1414 | } 1415 | //////////////////////////////////////////////////////////////////////////////// 1416 | 1417 | // Boilerplate to wrap and bind a callback. 1418 | // The resulting callback must be stored for as long as it may be used. 1419 | #[must_use] 1420 | fn build_cb(f: F) -> JsClosure 1421 | where 1422 | F: FnMut(T) -> JsError + 'static, 1423 | T: FromWasmAbi + 'static, 1424 | { 1425 | Closure::wrap(Box::new(f) as Box JsError>) 1426 | } 1427 | 1428 | #[must_use] 1429 | fn set_event_cb(obj: &E, name: &str, f: F) -> JsClosure 1430 | where 1431 | E: JsCast + Clone + std::fmt::Debug, 1432 | F: FnMut(T) -> JsError + 'static, 1433 | T: FromWasmAbi + 'static, 1434 | { 1435 | let cb = build_cb(f); 1436 | let target = obj 1437 | .dyn_ref::() 1438 | .expect("Could not convert into `EventTarget`"); 1439 | target 1440 | .add_event_listener_with_callback(name, cb.as_ref().unchecked_ref()) 1441 | .expect("Could not add event listener"); 1442 | cb 1443 | } 1444 | 1445 | //////////////////////////////////////////////////////////////////////////////// 1446 | 1447 | impl Connecting { 1448 | fn on_connected(self) -> JsResult { 1449 | self.base 1450 | .doc 1451 | .get_element_by_id("disconnected_msg") 1452 | .expect("Could not get disconnected_msg div") 1453 | .dyn_into::()? 1454 | .set_text_content(Some("Lost connection to game server")); 1455 | CreateOrJoin::new(self.base) 1456 | } 1457 | } 1458 | 1459 | impl CreateOrJoin { 1460 | fn new(base: Base) -> JsResult { 1461 | let name_input = base 1462 | .doc 1463 | .get_element_by_id("name_input") 1464 | .expect("Could not find name_input") 1465 | .dyn_into::()?; 1466 | let room_input = base 1467 | .doc 1468 | .get_element_by_id("room_input") 1469 | .expect("Could not find room_input") 1470 | .dyn_into::()?; 1471 | let room_invalid_cb = 1472 | set_event_cb(&room_input, "invalid", move |_: Event| { 1473 | HANDLE.lock().unwrap().on_room_name_invalid() 1474 | }); 1475 | let input_cb = set_event_cb(&room_input, "input", move |_: Event| { 1476 | HANDLE.lock().unwrap().on_join_inputs_changed() 1477 | }); 1478 | 1479 | let form = base 1480 | .doc 1481 | .get_element_by_id("join_form") 1482 | .expect("Could not find join_form"); 1483 | let submit_cb = set_event_cb(&form, "submit", move |e: Event| { 1484 | e.prevent_default(); 1485 | HANDLE.lock().unwrap().on_join_button() 1486 | }); 1487 | 1488 | let err_div = base 1489 | .doc 1490 | .get_element_by_id("err_div") 1491 | .expect("Could not find err_div") 1492 | .dyn_into()?; 1493 | let err_span = base 1494 | .doc 1495 | .get_element_by_id("err_span") 1496 | .expect("Could not find err_span") 1497 | .dyn_into()?; 1498 | 1499 | let play_button = base 1500 | .doc 1501 | .get_element_by_id("play_button") 1502 | .expect("Could not find play_button") 1503 | .dyn_into::()?; 1504 | 1505 | play_button.set_text_content(Some(if room_input.value().is_empty() { 1506 | "Create new room" 1507 | } else { 1508 | "Join existing room" 1509 | })); 1510 | play_button.class_list().remove_1("disabled")?; 1511 | 1512 | let colorblind_checkbox = base 1513 | .doc 1514 | .get_element_by_id("colorblind") 1515 | .expect("Could not find colorblind checkbox") 1516 | .dyn_into()?; 1517 | 1518 | Ok(CreateOrJoin { 1519 | base, 1520 | name_input, 1521 | room_input, 1522 | play_button, 1523 | colorblind_checkbox, 1524 | err_div, 1525 | err_span, 1526 | 1527 | _input_cb: input_cb, 1528 | _submit_cb: submit_cb, 1529 | _room_invalid_cb: room_invalid_cb, 1530 | }) 1531 | } 1532 | 1533 | fn on_join_failed(&self, err: &str) -> JsError { 1534 | self.err_span.set_text_content(Some(err)); 1535 | self.err_div.set_hidden(false); 1536 | self.play_button.set_disabled(false); 1537 | Ok(()) 1538 | } 1539 | 1540 | fn on_joined_room( 1541 | self, 1542 | room_name: &str, 1543 | players: &[(String, u32, bool)], 1544 | active_player: usize, 1545 | player_index: usize, 1546 | board: &[((i32, i32), Piece)], 1547 | pieces: &[Piece], 1548 | ) -> JsResult { 1549 | self.base 1550 | .doc 1551 | .get_element_by_id("join") 1552 | .expect("Could not get join div") 1553 | .dyn_into::()? 1554 | .set_hidden(true); 1555 | self.base 1556 | .doc 1557 | .get_element_by_id("playing") 1558 | .expect("Could not get playing div") 1559 | .dyn_into::()? 1560 | .set_hidden(false); 1561 | 1562 | let mut p = Playing::new( 1563 | self.base, 1564 | room_name, 1565 | players, 1566 | active_player, 1567 | player_index, 1568 | board, 1569 | pieces, 1570 | )?; 1571 | p.on_information(&format!("Welcome, {}!", players[player_index].0))?; 1572 | p.on_player_turn(active_player)?; 1573 | Ok(p) 1574 | } 1575 | 1576 | fn on_join_button(&self) -> JsError { 1577 | self.play_button.set_disabled(true); 1578 | let name = self.name_input.value(); 1579 | let room = self.room_input.value(); 1580 | if self.colorblind_checkbox.checked() { 1581 | self.base 1582 | .doc 1583 | .get_element_by_id("playing") 1584 | .ok_or_else(|| JsValue::from_str("No playing box"))? 1585 | .class_list() 1586 | .add_1("colorblind")?; 1587 | } 1588 | let msg = if room.is_empty() { 1589 | ClientMessage::CreateRoom(name) 1590 | } else { 1591 | ClientMessage::JoinRoom(name, room) 1592 | }; 1593 | self.base.send(msg) 1594 | } 1595 | 1596 | fn on_join_inputs_changed(&self) -> JsError { 1597 | self.play_button.set_text_content(Some( 1598 | if self.room_input.value().is_empty() { 1599 | "Create new room" 1600 | } else { 1601 | "Join existing room" 1602 | }, 1603 | )); 1604 | self.room_input.set_custom_validity(""); 1605 | Ok(()) 1606 | } 1607 | 1608 | fn on_room_name_invalid(&self) -> JsError { 1609 | self.room_input.set_custom_validity("three lowercase words"); 1610 | Ok(()) 1611 | } 1612 | } 1613 | 1614 | //////////////////////////////////////////////////////////////////////////////// 1615 | 1616 | impl Playing { 1617 | fn new( 1618 | base: Base, 1619 | room_name: &str, 1620 | players: &[(String, u32, bool)], 1621 | active_player: usize, 1622 | player_index: usize, 1623 | in_board: &[((i32, i32), Piece)], 1624 | pieces: &[Piece], 1625 | ) -> JsResult { 1626 | // The title lists the room name 1627 | let s: HtmlElement = base 1628 | .doc 1629 | .get_element_by_id("room_name") 1630 | .expect("Could not get room_name") 1631 | .dyn_into()?; 1632 | s.set_text_content(Some(room_name)); 1633 | 1634 | let board = Board::new(&base.doc)?; 1635 | 1636 | let b = base 1637 | .doc 1638 | .get_element_by_id("chat_name") 1639 | .expect("Could not get chat_name"); 1640 | b.set_text_content(Some(&format!("{}:", players[player_index].0))); 1641 | 1642 | // If Enter is pressed while focus is in the chat box, 1643 | // send a chat message to the server. 1644 | let chat_input = base 1645 | .doc 1646 | .get_element_by_id("chat_input") 1647 | .expect("Could not get chat_input") 1648 | .dyn_into()?; 1649 | let keyup_cb = 1650 | set_event_cb(&chat_input, "keyup", move |e: KeyboardEvent| { 1651 | if e.key_code() == 13 { 1652 | // Enter key 1653 | e.prevent_default(); 1654 | HANDLE.lock().unwrap().on_send_chat() 1655 | } else { 1656 | Ok(()) 1657 | } 1658 | }); 1659 | 1660 | let chat_div = base 1661 | .doc 1662 | .get_element_by_id("chat_msgs") 1663 | .expect("Could not get chat_div") 1664 | .dyn_into()?; 1665 | let score_table = base 1666 | .doc 1667 | .get_element_by_id("score_rows") 1668 | .expect("Could not get score_rows") 1669 | .dyn_into()?; 1670 | 1671 | let mut out = Playing { 1672 | base, 1673 | board, 1674 | 1675 | chat_input, 1676 | chat_div, 1677 | score_table, 1678 | player_index, 1679 | active_player, 1680 | player_names: Vec::new(), 1681 | 1682 | _keyup_cb: keyup_cb, 1683 | }; 1684 | 1685 | for ((x, y), p) in in_board.iter() { 1686 | out.board.add_piece(*p, *x, *y)?; 1687 | } 1688 | for p in pieces.iter() { 1689 | out.board.add_hand(*p)?; 1690 | } 1691 | 1692 | for (i, (name, score, connected)) in players.iter().enumerate() { 1693 | out.add_player_row( 1694 | name, 1695 | *score as usize, 1696 | *connected, 1697 | i == player_index, 1698 | )?; 1699 | } 1700 | 1701 | let s = out 1702 | .score_table 1703 | .child_nodes() 1704 | .item(out.player_index as u32 + 3) 1705 | .expect("Could not get table row") 1706 | .child_nodes() 1707 | .item(2) 1708 | .expect("Could not get score value") 1709 | .child_nodes() 1710 | .item(1) 1711 | .expect("Could not get second span") 1712 | .dyn_into::()?; 1713 | out.board.tentative_score_span = Some(s); 1714 | 1715 | Ok(out) 1716 | } 1717 | 1718 | fn on_chat(&self, from: &str, msg: &str) -> JsError { 1719 | let p = self.base.doc.create_element("p")?; 1720 | p.set_class_name("msg"); 1721 | 1722 | let b = self.base.doc.create_element("b")?; 1723 | b.set_text_content(Some(from)); 1724 | p.append_child(&b)?; 1725 | 1726 | let s = self.base.doc.create_element("b")?; 1727 | s.set_text_content(Some(":")); 1728 | p.append_child(&s)?; 1729 | 1730 | let s = self.base.doc.create_element("span")?; 1731 | s.set_text_content(Some(msg)); 1732 | p.append_child(&s)?; 1733 | 1734 | self.chat_div.append_child(&p)?; 1735 | self.chat_div.set_scroll_top(self.chat_div.scroll_height()); 1736 | Ok(()) 1737 | } 1738 | 1739 | fn on_information(&self, msg: &str) -> JsError { 1740 | let p = self.base.doc.create_element("p")?; 1741 | p.set_class_name("msg"); 1742 | 1743 | let i = self.base.doc.create_element("i")?; 1744 | i.set_text_content(Some(msg)); 1745 | p.append_child(&i)?; 1746 | self.chat_div.append_child(&p)?; 1747 | self.chat_div.set_scroll_top(self.chat_div.scroll_height()); 1748 | Ok(()) 1749 | } 1750 | 1751 | fn add_player_row( 1752 | &mut self, 1753 | name: &str, 1754 | score: usize, 1755 | connected: bool, 1756 | is_you: bool, 1757 | ) -> JsError { 1758 | let tr = self.base.doc.create_element("tr")?; 1759 | tr.set_class_name("player-row"); 1760 | 1761 | let td = self.base.doc.create_element("td")?; 1762 | let i = self.base.doc.create_element("i")?; 1763 | i.set_class_name("fas fa-caret-right"); 1764 | td.append_child(&i)?; 1765 | tr.append_child(&td)?; 1766 | 1767 | let td = self.base.doc.create_element("td")?; 1768 | if is_you { 1769 | td.set_text_content(Some(&format!("{} (you)", name))); 1770 | } else { 1771 | td.set_text_content(Some(name)); 1772 | } 1773 | tr.append_child(&td)?; 1774 | 1775 | let td = self.base.doc.create_element("td")?; 1776 | let score_span = self.base.doc.create_element("span")?; 1777 | score_span.set_text_content(Some(&score.to_string())); 1778 | td.append_child(&score_span)?; 1779 | if is_you { 1780 | let new_score = self.base.doc.create_element("span")?; 1781 | td.append_child(&new_score)?; 1782 | } 1783 | tr.append_child(&td)?; 1784 | 1785 | if !connected { 1786 | tr.class_list().add_1("disconnected")?; 1787 | } 1788 | 1789 | self.score_table.append_child(&tr)?; 1790 | self.player_names.push(name.to_string()); 1791 | 1792 | Ok(()) 1793 | } 1794 | 1795 | fn on_send_chat(&self) -> JsError { 1796 | let i = self.chat_input.value(); 1797 | if !i.is_empty() { 1798 | self.chat_input.set_value(""); 1799 | self.base.send(ClientMessage::Chat(i)) 1800 | } else { 1801 | Ok(()) 1802 | } 1803 | } 1804 | 1805 | fn on_new_player(&mut self, name: &str) -> JsError { 1806 | // Append a player to the bottom of the scores list 1807 | self.add_player_row(name, 0, true, false)?; 1808 | self.on_information(&format!("{} joined the room", name)) 1809 | } 1810 | 1811 | fn on_player_disconnected(&self, index: usize) -> JsError { 1812 | let c = self 1813 | .score_table 1814 | .child_nodes() 1815 | .item((index + 3) as u32) 1816 | .unwrap() 1817 | .dyn_into::()?; 1818 | c.class_list().add_1("disconnected")?; 1819 | self.on_information(&format!( 1820 | "{} disconnected", 1821 | self.player_names[index] 1822 | )) 1823 | } 1824 | 1825 | fn on_player_reconnected(&self, index: usize) -> JsError { 1826 | let c = self 1827 | .score_table 1828 | .child_nodes() 1829 | .item((index + 3) as u32) 1830 | .unwrap() 1831 | .dyn_into::()?; 1832 | c.class_list().remove_1("disconnected")?; 1833 | self.on_information(&format!( 1834 | "{} reconnected", 1835 | self.player_names[index] 1836 | )) 1837 | } 1838 | 1839 | fn on_player_turn(&mut self, active_player: usize) -> JsError { 1840 | let children = self.score_table.child_nodes(); 1841 | children 1842 | .item((self.active_player + 3) as u32) 1843 | .unwrap() 1844 | .dyn_into::()? 1845 | .class_list() 1846 | .remove_1("active")?; 1847 | 1848 | self.active_player = active_player; 1849 | children 1850 | .item((self.active_player + 3) as u32) 1851 | .unwrap() 1852 | .dyn_into::()? 1853 | .class_list() 1854 | .add_1("active")?; 1855 | 1856 | if self.active_player == self.player_index { 1857 | self.on_information("It's your turn!") 1858 | } else { 1859 | self.on_information(&format!( 1860 | "It's {}'s turn!", 1861 | self.player_names[self.active_player] 1862 | )) 1863 | }?; 1864 | 1865 | self.board.set_my_turn(active_player == self.player_index) 1866 | } 1867 | 1868 | fn on_anim(&mut self, t: f64) -> JsError { 1869 | self.board.on_anim(t) 1870 | } 1871 | 1872 | fn on_pan_start(&mut self, evt: PointerEvent) -> JsError { 1873 | self.board.on_pan_start(evt) 1874 | } 1875 | 1876 | fn on_pan_move(&mut self, evt: PointerEvent) -> JsError { 1877 | self.board.on_pan_move(evt) 1878 | } 1879 | 1880 | fn on_pan_end(&mut self, evt: Event) -> JsError { 1881 | self.board.on_pan_end(evt) 1882 | } 1883 | 1884 | fn on_pointer_down(&mut self, evt: PointerEvent) -> JsError { 1885 | self.board.on_pointer_down(evt) 1886 | } 1887 | 1888 | fn on_pointer_move(&mut self, evt: PointerEvent) -> JsError { 1889 | self.board.on_pointer_move(evt) 1890 | } 1891 | 1892 | fn on_pointer_up(&mut self, evt: PointerEvent) -> JsError { 1893 | self.board.on_pointer_up(evt) 1894 | } 1895 | 1896 | fn on_reject_button(&mut self, evt: Event) -> JsError { 1897 | self.board.on_reject_button(evt) 1898 | } 1899 | 1900 | fn on_accept_button(&mut self, evt: Event) -> JsError { 1901 | match self.board.make_move(evt)? { 1902 | Move::Place(m) => self.base.send(ClientMessage::Play(m)), 1903 | Move::Swap(m) => self.base.send(ClientMessage::Swap(m)), 1904 | } 1905 | } 1906 | 1907 | fn on_played(&mut self, pieces: &[(Piece, i32, i32)]) -> JsError { 1908 | self.board.reset_played()?; 1909 | let mut anims = Vec::new(); 1910 | let t0 = get_time_ms(); 1911 | for (piece, x, y) in pieces { 1912 | let target = self.board.add_piece(*piece, *x, *y)?; 1913 | anims.push(TileAnimation { 1914 | target, 1915 | start: (225.0, *y as f32 * 10.0), 1916 | end: (*x as f32 * 10.0, *y as f32 * 10.0), 1917 | t0, 1918 | }); 1919 | } 1920 | // If we're panning, we need to cancel the pan state before starting 1921 | // an animation, otherwise a mouse-up will mess things up. 1922 | if let BoardState::Panning(_) = &self.board.state { 1923 | self.board.on_pan_end(Event::new("CancelPan")?)?; 1924 | } else if let BoardState::Dragging(d) = &self.board.state { 1925 | // If the user is dragging a piece around their hand, then inject 1926 | // an animation to slide the tile back to its original position 1927 | self.board.pan_group.remove_child(&d.shadow)?; 1928 | self.board.release_drag_captures(d)?; 1929 | 1930 | // Parse the current position from the transform attribute 1931 | let transform = d 1932 | .target 1933 | .get_attribute("transform") 1934 | .unwrap_or_else(|| "translate(0 0)".to_string()); 1935 | let pos = &transform[10..transform.len() - 1]; 1936 | let mut itr = pos.split(' '); 1937 | let x: f32 = itr.next().unwrap_or("0").parse().unwrap_or(0.0); 1938 | let y: f32 = itr.next().unwrap_or("0").parse().unwrap_or(0.0); 1939 | anims.push(TileAnimation { 1940 | target: d.target.clone(), 1941 | start: (x, y), 1942 | end: ((d.hand_index * 15 + 5) as f32, 185.0), 1943 | t0, 1944 | }); 1945 | } 1946 | self.board.state = BoardState::Animation(DragAnim::DropManyToGrid( 1947 | DropManyToGrid(anims), 1948 | )); 1949 | self.board.request_animation_frame()?; 1950 | Ok(()) 1951 | } 1952 | 1953 | fn active_player_name(&self) -> &str { 1954 | if self.active_player == self.player_index { 1955 | "You" 1956 | } else { 1957 | &self.player_names[self.active_player] 1958 | } 1959 | } 1960 | 1961 | fn on_swapped(&mut self, count: usize) -> JsError { 1962 | self.on_information(&format!( 1963 | "{} swapped {} piece{}", 1964 | self.active_player_name(), 1965 | count, 1966 | if count > 1 { "s" } else { "" } 1967 | )) 1968 | } 1969 | 1970 | fn on_move_accepted(&mut self, dealt: &[Piece]) -> JsError { 1971 | self.board.on_move_accepted(dealt) 1972 | } 1973 | 1974 | fn on_move_rejected(&mut self) -> JsError { 1975 | Ok(()) 1976 | } 1977 | 1978 | fn on_player_score(&mut self, delta: u32, total: u32) -> JsError { 1979 | self.score_table 1980 | .child_nodes() 1981 | .item(self.active_player as u32 + 3) 1982 | .expect("Could not get table row") 1983 | .child_nodes() 1984 | .item(2) 1985 | .expect("Could not get score value") 1986 | .child_nodes() 1987 | .item(0) 1988 | .expect("Could not get first span") 1989 | .set_text_content(Some(&total.to_string())); 1990 | self.on_information(&format!( 1991 | "{} scored {} point{}", 1992 | self.active_player_name(), 1993 | delta, 1994 | if delta == 1 { "" } else { "s" } 1995 | )) 1996 | } 1997 | 1998 | fn on_finished(&mut self, winner: usize) -> JsError { 1999 | self.board.set_my_turn(false)?; 2000 | 2001 | let children = self.score_table.child_nodes(); 2002 | children 2003 | .item((self.active_player + 3) as u32) 2004 | .unwrap() 2005 | .dyn_into::()? 2006 | .class_list() 2007 | .remove_1("active")?; 2008 | 2009 | if winner == self.player_index { 2010 | self.on_information("You win!") 2011 | } else { 2012 | self.on_information(&format!("{} wins!", self.player_names[winner])) 2013 | } 2014 | } 2015 | 2016 | fn on_pieces_remaining(&mut self, remaining: usize) -> JsError { 2017 | self.board.pieces_remaining = remaining; 2018 | self.board.update_count_div()?; 2019 | self.board.update_exchange_div(self.board.my_turn) 2020 | } 2021 | } 2022 | 2023 | //////////////////////////////////////////////////////////////////////////////// 2024 | 2025 | fn on_message(msg: ServerMessage) -> JsError { 2026 | use ServerMessage::*; 2027 | console_log!("Got message {:?}", msg); 2028 | 2029 | let mut state = HANDLE.lock().unwrap(); 2030 | 2031 | match msg { 2032 | JoinFailed(name) => state.on_join_failed(&name), 2033 | JoinedRoom { 2034 | room_name, 2035 | players, 2036 | active_player, 2037 | player_index, 2038 | board, 2039 | pieces, 2040 | } => state.on_joined_room( 2041 | &room_name, 2042 | &players, 2043 | active_player, 2044 | player_index, 2045 | &board, 2046 | &pieces, 2047 | ), 2048 | Chat { from, message } => state.on_chat(&from, &message), 2049 | Information(message) => state.on_information(&message), 2050 | NewPlayer(name) => state.on_new_player(&name), 2051 | PlayerDisconnected(index) => state.on_player_disconnected(index), 2052 | PlayerReconnected(index) => state.on_player_reconnected(index), 2053 | PlayerTurn(active_player) => state.on_player_turn(active_player), 2054 | PiecesRemaining(remaining) => state.on_pieces_remaining(remaining), 2055 | Played(pieces) => state.on_played(&pieces), 2056 | Swapped(count) => state.on_swapped(count), 2057 | MoveAccepted(dealt) => state.on_move_accepted(&dealt), 2058 | MoveRejected => state.on_move_rejected(), 2059 | PlayerScore { delta, total } => state.on_player_score(delta, total), 2060 | ItsOver(winner) => state.on_finished(winner), 2061 | } 2062 | } 2063 | 2064 | //////////////////////////////////////////////////////////////////////////////// 2065 | 2066 | // Called when the wasm module is instantiated 2067 | #[wasm_bindgen(start)] 2068 | pub fn main() -> JsError { 2069 | console_error_panic_hook::set_once(); 2070 | 2071 | let window = web_sys::window().expect("no global `window` exists"); 2072 | let doc = window.document().expect("should have a document on window"); 2073 | 2074 | let location = doc.location().expect("Could not get doc location"); 2075 | let hostname = location.hostname()?; 2076 | 2077 | // Pick the port based on the connection type 2078 | let (ws_protocol, ws_port) = if location.protocol()? == "https:" { 2079 | ("wss", 8081) 2080 | } else { 2081 | ("ws", 8080) 2082 | }; 2083 | let hostname = format!("{}://{}:{}", ws_protocol, hostname, ws_port); 2084 | 2085 | let doc = web_sys::window() 2086 | .expect("no global `window` exists") 2087 | .document() 2088 | .expect("should have a document on window"); 2089 | console_log!("Connecting to websocket at {}", hostname); 2090 | let ws = WebSocket::new(&hostname)?; 2091 | 2092 | // The websocket callbacks are long-lived, so we forget them here 2093 | set_event_cb(&ws, "open", move |_: JsValue| { 2094 | HANDLE.lock().unwrap().on_connected() 2095 | }) 2096 | .forget(); 2097 | let on_decoded_cb = Closure::wrap(Box::new(move |e: ProgressEvent| { 2098 | let target = e.target().expect("Could not get target"); 2099 | let reader: FileReader = target.dyn_into().expect("Could not cast"); 2100 | let result = reader.result().expect("Could not get result"); 2101 | let buf = js_sys::Uint8Array::new(&result); 2102 | let mut data = vec![0; buf.length() as usize]; 2103 | buf.copy_to(&mut data[..]); 2104 | let msg = bincode::deserialize(&data[..]) 2105 | .map_err(|e| { 2106 | JsValue::from_str(&format!("Failed to deserialize: {}", e)) 2107 | }) 2108 | .expect("Could not decode message"); 2109 | on_message(msg).expect("Message decoding failed") 2110 | }) as Box); 2111 | set_event_cb(&ws, "message", move |e: MessageEvent| { 2112 | let blob = e.data().dyn_into::()?; 2113 | let fr = FileReader::new()?; 2114 | fr.add_event_listener_with_callback( 2115 | "load", 2116 | on_decoded_cb.as_ref().unchecked_ref(), 2117 | )?; 2118 | fr.read_as_array_buffer(&blob)?; 2119 | Ok(()) 2120 | }) 2121 | .forget(); 2122 | set_event_cb(&ws, "close", move |_: Event| -> JsError { 2123 | let doc = web_sys::window() 2124 | .expect("no global `window` exists") 2125 | .document() 2126 | .expect("should have a document on window"); 2127 | for d in ["join", "playing"].iter() { 2128 | doc.get_element_by_id(d) 2129 | .expect("Could not get major div") 2130 | .dyn_into::()? 2131 | .set_hidden(true); 2132 | } 2133 | doc.get_element_by_id("disconnected") 2134 | .expect("Could not get disconnected div") 2135 | .dyn_into::()? 2136 | .set_hidden(false); 2137 | Ok(()) 2138 | }) 2139 | .forget(); 2140 | 2141 | let rev = doc 2142 | .get_element_by_id("revhash") 2143 | .expect("Could not find rev"); 2144 | rev.set_text_content(Some(env!("VERGEN_SHA_SHORT"))); 2145 | 2146 | let base = Base { doc, ws }; 2147 | base.doc 2148 | .get_element_by_id("play_button") 2149 | .expect("Could not get loading div") 2150 | .dyn_into::()? 2151 | .set_text_content(Some("Connecting...")); 2152 | 2153 | *HANDLE.lock().unwrap() = State::Connecting(Connecting { base }); 2154 | 2155 | Ok(()) 2156 | } 2157 | -------------------------------------------------------------------------------- /pont-common/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "cfg-if" 5 | version = "0.1.10" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 8 | 9 | [[package]] 10 | name = "getrandom" 11 | version = "0.1.14" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 14 | dependencies = [ 15 | "cfg-if", 16 | "libc", 17 | "wasi", 18 | ] 19 | 20 | [[package]] 21 | name = "libc" 22 | version = "0.2.69" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 25 | 26 | [[package]] 27 | name = "pont-common" 28 | version = "0.1.0" 29 | dependencies = [ 30 | "rand", 31 | "serde", 32 | "serde_derive", 33 | ] 34 | 35 | [[package]] 36 | name = "ppv-lite86" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 40 | 41 | [[package]] 42 | name = "proc-macro2" 43 | version = "1.0.10" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 46 | dependencies = [ 47 | "unicode-xid", 48 | ] 49 | 50 | [[package]] 51 | name = "quote" 52 | version = "1.0.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 55 | dependencies = [ 56 | "proc-macro2", 57 | ] 58 | 59 | [[package]] 60 | name = "rand" 61 | version = "0.7.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 64 | dependencies = [ 65 | "getrandom", 66 | "libc", 67 | "rand_chacha", 68 | "rand_core", 69 | "rand_hc", 70 | ] 71 | 72 | [[package]] 73 | name = "rand_chacha" 74 | version = "0.2.2" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 77 | dependencies = [ 78 | "ppv-lite86", 79 | "rand_core", 80 | ] 81 | 82 | [[package]] 83 | name = "rand_core" 84 | version = "0.5.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 87 | dependencies = [ 88 | "getrandom", 89 | ] 90 | 91 | [[package]] 92 | name = "rand_hc" 93 | version = "0.2.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 96 | dependencies = [ 97 | "rand_core", 98 | ] 99 | 100 | [[package]] 101 | name = "serde" 102 | version = "1.0.106" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 105 | dependencies = [ 106 | "serde_derive", 107 | ] 108 | 109 | [[package]] 110 | name = "serde_derive" 111 | version = "1.0.106" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 114 | dependencies = [ 115 | "proc-macro2", 116 | "quote", 117 | "syn", 118 | ] 119 | 120 | [[package]] 121 | name = "syn" 122 | version = "1.0.17" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "unicode-xid", 129 | ] 130 | 131 | [[package]] 132 | name = "unicode-xid" 133 | version = "0.2.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 136 | 137 | [[package]] 138 | name = "wasi" 139 | version = "0.9.0+wasi-snapshot-preview1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 142 | -------------------------------------------------------------------------------- /pont-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pont-common" 3 | version = "0.1.0" 4 | authors = ["Matt Keeter "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | serde = { version = "^1.0.59", features = ["derive"] } 12 | serde_derive = "^1.0.59" 13 | rand = "*" 14 | -------------------------------------------------------------------------------- /pont-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use serde::{Serialize, Deserialize}; 3 | 4 | use rand::thread_rng; 5 | use rand::seq::SliceRandom; 6 | 7 | #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] 8 | pub enum ClientMessage { 9 | CreateRoom(String), 10 | JoinRoom(String, String), 11 | Chat(String), 12 | Play(Vec<(Piece, i32, i32)>), 13 | Swap(Vec), 14 | 15 | Disconnected, 16 | } 17 | 18 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 19 | pub enum ServerMessage { 20 | JoinedRoom { 21 | room_name: String, 22 | players: Vec<(String, u32, bool)>, 23 | active_player: usize, 24 | player_index: usize, 25 | board: Vec<((i32, i32), Piece)>, 26 | pieces: Vec, 27 | }, 28 | JoinFailed(String), 29 | Chat { 30 | from: String, 31 | message: String, 32 | }, 33 | Information(String), 34 | NewPlayer(String), 35 | PlayerReconnected(usize), 36 | PlayerDisconnected(usize), 37 | PlayerTurn(usize), 38 | Played(Vec<(Piece, i32, i32)>), 39 | Swapped(usize), 40 | MoveAccepted(Vec), 41 | MoveRejected, 42 | PlayerScore { 43 | delta: u32, 44 | total: u32, 45 | }, 46 | PiecesRemaining(usize), 47 | ItsOver(usize), 48 | } 49 | 50 | #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 51 | pub enum Shape { 52 | Clover, 53 | Star, 54 | Square, 55 | Diamond, 56 | Cross, 57 | Circle, 58 | } 59 | 60 | #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 61 | pub enum Color { 62 | Orange, 63 | Yellow, 64 | Green, 65 | Red, 66 | Blue, 67 | Purple, 68 | } 69 | 70 | pub type Piece = (Shape, Color); 71 | 72 | #[derive(Debug, Deserialize, Serialize)] 73 | pub struct Game { 74 | pub board: HashMap<(i32, i32), Piece>, 75 | pub bag: Vec, 76 | } 77 | 78 | impl Game { 79 | pub fn play(&mut self, ps: &[(Piece, i32, i32)]) -> Option { 80 | for (p, x, y) in ps { 81 | use std::collections::hash_map::Entry; 82 | match self.board.entry((*x, *y)) { 83 | Entry::Occupied(_) => return None, 84 | Entry::Vacant(v) => { v.insert(*p); } 85 | } 86 | } 87 | let mut score = 0; 88 | let mut seen_rows = HashSet::new(); 89 | let mut seen_cols = HashSet::new(); 90 | let f = |v: Vec<(Piece, (i32, i32))>| -> Vec<(i32, i32)> { 91 | let mut v = v.into_iter() 92 | .map(|(_p, (x, y))| (x, y)) 93 | .collect::>(); 94 | v.sort(); 95 | v 96 | }; 97 | for (_piece, x, y) in ps { 98 | let row = f(Self::explore_from(&self.board, |i| (*x + i, *y))); 99 | if row.len() > 1 && seen_rows.insert(row[0]) { 100 | score += row.len(); 101 | if row.len() == 6 { 102 | score += 6; 103 | } 104 | } 105 | 106 | let col = f(Self::explore_from(&self.board, |i| (*x, *y + i))); 107 | if col.len() > 1 && seen_cols.insert(col[0]) { 108 | score += col.len(); 109 | if col.len() == 6 { 110 | score += 6; 111 | } 112 | } 113 | } 114 | Some(score as u32) 115 | } 116 | 117 | pub fn shuffle(&mut self) { 118 | self.bag.shuffle(&mut thread_rng()); 119 | } 120 | 121 | pub fn deal(&mut self, n: usize) -> HashMap { 122 | let mut out = HashMap::new(); 123 | for _ in 0..n { 124 | if let Some(p) = self.bag.pop() { 125 | *out.entry(p).or_insert(0) += 1; 126 | } 127 | } 128 | out 129 | } 130 | 131 | pub fn swap(&mut self, pieces: &[Piece]) -> Option> { 132 | if pieces.len() <= self.bag.len() { 133 | let mut out = Vec::new(); 134 | for _ in 0..pieces.len() { 135 | out.push(self.bag.pop().unwrap()); 136 | } 137 | for p in pieces.iter() { 138 | self.bag.push(*p); 139 | } 140 | self.bag.shuffle(&mut thread_rng()); 141 | Some(out) 142 | } else { 143 | None 144 | } 145 | } 146 | 147 | fn connected(board: &HashMap<(i32, i32), Piece>) -> bool { 148 | let mut todo: Vec<(i32, i32)> = 149 | board.keys().take(1).cloned().collect(); 150 | 151 | let mut seen = HashSet::new(); 152 | while let Some(t) = todo.pop() { 153 | if seen.insert(t) { 154 | for (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)].iter() { 155 | let c = (t.0 + dx, t.1 + dy); 156 | if board.contains_key(&c) { 157 | todo.push(c); 158 | } 159 | } 160 | } 161 | } 162 | 163 | seen.len() == board.len() 164 | } 165 | 166 | // Checks whether the given play is linear and connected 167 | // 168 | // The board must already include the pieces in played 169 | pub fn is_linear_connected(board: &HashMap<(i32, i32), Piece>, 170 | played: &[(i32, i32)]) -> bool { 171 | let xmin = played.iter().map(|p| p.0).min().unwrap_or(0); 172 | let ymin = played.iter().map(|p| p.1).min().unwrap_or(0); 173 | let xmax = played.iter().map(|p| p.0).max().unwrap_or(0); 174 | let ymax = played.iter().map(|p| p.1).max().unwrap_or(0); 175 | 176 | // Fail if the play isn't constrained to a single row/column 177 | if xmin != xmax && ymin != ymax { 178 | return false; 179 | } 180 | for x in xmin..=xmax { 181 | for y in ymin..=ymax { 182 | if !board.contains_key(&(x, y)) { 183 | return false; 184 | } 185 | } 186 | } 187 | true 188 | } 189 | 190 | fn explore_from(board: &HashMap<(i32, i32), Piece>, f: T) 191 | -> Vec<(Piece, (i32, i32))> 192 | where T: Fn(i32) -> (i32, i32) 193 | { 194 | let mut out = Vec::new(); 195 | let mut run = |g: &dyn Fn(i32) -> i32| { 196 | for i in 0.. { 197 | let c = f(g(i)); 198 | if let Some(piece) = board.get(&c) { 199 | out.push((*piece, c)); 200 | } else { 201 | break; 202 | } 203 | } 204 | }; 205 | run(&|i| i); 206 | run(&|i| (-i - 1)); 207 | out 208 | } 209 | 210 | // Checks whether the given board is valid, 211 | // returning a vec of invalid piece locations 212 | pub fn invalid(board: &HashMap<(i32, i32), Piece>) -> HashSet<(i32, i32)> { 213 | // The empty board has no invalid pieces, by definition 214 | if board.is_empty() { 215 | return HashSet::new(); 216 | } 217 | 218 | // If a board has disconnected components, then it's all invalid 219 | let todo = board.keys().cloned().collect(); 220 | if !Self::connected(board) { 221 | return todo; 222 | } 223 | 224 | let mut checked_h = HashSet::new(); 225 | let mut checked_v = HashSet::new(); 226 | 227 | let mut out = HashSet::new(); 228 | 229 | let check = |pieces: &[(Piece, (i32, i32))]| -> bool { 230 | let mut seen_colors = HashSet::new(); 231 | let mut seen_shapes = HashSet::new(); 232 | let mut seen_pieces = HashSet::new(); 233 | for (piece, _pos) in pieces { 234 | // Detect duplicate pieces 235 | if !seen_pieces.insert(*piece) { 236 | return false; 237 | } 238 | seen_colors.insert(piece.0); 239 | seen_shapes.insert(piece.1); 240 | } 241 | seen_colors.len() == 1 || seen_shapes.len() == 1 242 | }; 243 | 244 | // Check that each row and column contains valid pieces 245 | for (x, y) in todo.into_iter() { 246 | if !checked_h.contains(&(x, y)) { 247 | let row = Self::explore_from(board, |i| (x + i, y)); 248 | for (_, c) in row.iter() { 249 | checked_h.insert(*c); 250 | } 251 | if !check(&row) { 252 | for (_, c) in row.into_iter() { 253 | out.insert(c); 254 | } 255 | } 256 | } 257 | if !checked_v.contains(&(x, y)) { 258 | let col = Self::explore_from(board, |i| (x, y + i)); 259 | for (_, c) in col.iter() { 260 | checked_v.insert(*c); 261 | } 262 | if !check(&col) { 263 | for (_, c) in col.into_iter() { 264 | out.insert(c); 265 | } 266 | } 267 | } 268 | } 269 | out 270 | } 271 | } 272 | 273 | impl Default for Game { 274 | fn default() -> Game { 275 | use Color::*; 276 | use Shape::*; 277 | let mut bag = Vec::new(); 278 | for c in &[Orange, Yellow, Green, Red, Blue, Purple] { 279 | for s in &[Clover, Star, Square, Diamond, Cross, Circle] { 280 | for _ in 0..3 { 281 | bag.push((*s, *c)); 282 | } 283 | } 284 | } 285 | bag.shuffle(&mut thread_rng()); 286 | 287 | Game { 288 | board: HashMap::new(), bag 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /pont-server/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.10" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.28" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff" 19 | 20 | [[package]] 21 | name = "async-task" 22 | version = "3.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" 25 | 26 | [[package]] 27 | name = "async-tungstenite" 28 | version = "0.4.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "4187bb446c8ecb8849f17cef7553db8bdb09e482e806257130189958fb42dca7" 31 | dependencies = [ 32 | "futures-io", 33 | "futures-util", 34 | "log", 35 | "pin-project", 36 | "tungstenite", 37 | ] 38 | 39 | [[package]] 40 | name = "atty" 41 | version = "0.2.14" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 44 | dependencies = [ 45 | "hermit-abi", 46 | "libc", 47 | "winapi", 48 | ] 49 | 50 | [[package]] 51 | name = "autocfg" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 55 | 56 | [[package]] 57 | name = "base64" 58 | version = "0.11.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 61 | 62 | [[package]] 63 | name = "bincode" 64 | version = "1.3.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 67 | dependencies = [ 68 | "serde", 69 | ] 70 | 71 | [[package]] 72 | name = "bitflags" 73 | version = "1.2.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 76 | 77 | [[package]] 78 | name = "block-buffer" 79 | version = "0.7.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" 82 | dependencies = [ 83 | "block-padding", 84 | "byte-tools", 85 | "byteorder", 86 | "generic-array", 87 | ] 88 | 89 | [[package]] 90 | name = "block-padding" 91 | version = "0.1.5" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 94 | dependencies = [ 95 | "byte-tools", 96 | ] 97 | 98 | [[package]] 99 | name = "byte-tools" 100 | version = "0.3.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 103 | 104 | [[package]] 105 | name = "byteorder" 106 | version = "1.3.4" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 109 | 110 | [[package]] 111 | name = "bytes" 112 | version = "0.5.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" 115 | 116 | [[package]] 117 | name = "cc" 118 | version = "1.0.50" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 121 | 122 | [[package]] 123 | name = "cfg-if" 124 | version = "0.1.10" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 127 | 128 | [[package]] 129 | name = "crossbeam" 130 | version = "0.7.3" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" 133 | dependencies = [ 134 | "cfg-if", 135 | "crossbeam-channel", 136 | "crossbeam-deque", 137 | "crossbeam-epoch", 138 | "crossbeam-queue", 139 | "crossbeam-utils", 140 | ] 141 | 142 | [[package]] 143 | name = "crossbeam-channel" 144 | version = "0.4.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" 147 | dependencies = [ 148 | "crossbeam-utils", 149 | "maybe-uninit", 150 | ] 151 | 152 | [[package]] 153 | name = "crossbeam-deque" 154 | version = "0.7.3" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" 157 | dependencies = [ 158 | "crossbeam-epoch", 159 | "crossbeam-utils", 160 | "maybe-uninit", 161 | ] 162 | 163 | [[package]] 164 | name = "crossbeam-epoch" 165 | version = "0.8.2" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" 168 | dependencies = [ 169 | "autocfg", 170 | "cfg-if", 171 | "crossbeam-utils", 172 | "lazy_static", 173 | "maybe-uninit", 174 | "memoffset", 175 | "scopeguard", 176 | ] 177 | 178 | [[package]] 179 | name = "crossbeam-queue" 180 | version = "0.2.1" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" 183 | dependencies = [ 184 | "cfg-if", 185 | "crossbeam-utils", 186 | ] 187 | 188 | [[package]] 189 | name = "crossbeam-utils" 190 | version = "0.7.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 193 | dependencies = [ 194 | "autocfg", 195 | "cfg-if", 196 | "lazy_static", 197 | ] 198 | 199 | [[package]] 200 | name = "digest" 201 | version = "0.8.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 204 | dependencies = [ 205 | "generic-array", 206 | ] 207 | 208 | [[package]] 209 | name = "env_logger" 210 | version = "0.7.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 213 | dependencies = [ 214 | "atty", 215 | "humantime", 216 | "log", 217 | "regex", 218 | "termcolor", 219 | ] 220 | 221 | [[package]] 222 | name = "fake-simd" 223 | version = "0.1.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 226 | 227 | [[package]] 228 | name = "fnv" 229 | version = "1.0.6" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" 232 | 233 | [[package]] 234 | name = "futures" 235 | version = "0.3.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" 238 | dependencies = [ 239 | "futures-channel", 240 | "futures-core", 241 | "futures-executor", 242 | "futures-io", 243 | "futures-sink", 244 | "futures-task", 245 | "futures-util", 246 | ] 247 | 248 | [[package]] 249 | name = "futures-channel" 250 | version = "0.3.4" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" 253 | dependencies = [ 254 | "futures-core", 255 | "futures-sink", 256 | ] 257 | 258 | [[package]] 259 | name = "futures-core" 260 | version = "0.3.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" 263 | 264 | [[package]] 265 | name = "futures-executor" 266 | version = "0.3.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" 269 | dependencies = [ 270 | "futures-core", 271 | "futures-task", 272 | "futures-util", 273 | ] 274 | 275 | [[package]] 276 | name = "futures-io" 277 | version = "0.3.4" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" 280 | 281 | [[package]] 282 | name = "futures-macro" 283 | version = "0.3.4" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" 286 | dependencies = [ 287 | "proc-macro-hack", 288 | "proc-macro2", 289 | "quote", 290 | "syn", 291 | ] 292 | 293 | [[package]] 294 | name = "futures-sink" 295 | version = "0.3.4" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" 298 | 299 | [[package]] 300 | name = "futures-task" 301 | version = "0.3.4" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" 304 | 305 | [[package]] 306 | name = "futures-util" 307 | version = "0.3.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" 310 | dependencies = [ 311 | "futures-channel", 312 | "futures-core", 313 | "futures-io", 314 | "futures-macro", 315 | "futures-sink", 316 | "futures-task", 317 | "memchr", 318 | "pin-utils", 319 | "proc-macro-hack", 320 | "proc-macro-nested", 321 | "slab", 322 | ] 323 | 324 | [[package]] 325 | name = "generic-array" 326 | version = "0.12.3" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" 329 | dependencies = [ 330 | "typenum", 331 | ] 332 | 333 | [[package]] 334 | name = "getrandom" 335 | version = "0.1.14" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 338 | dependencies = [ 339 | "cfg-if", 340 | "libc", 341 | "wasi", 342 | ] 343 | 344 | [[package]] 345 | name = "hermit-abi" 346 | version = "0.1.10" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" 349 | dependencies = [ 350 | "libc", 351 | ] 352 | 353 | [[package]] 354 | name = "http" 355 | version = "0.2.1" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" 358 | dependencies = [ 359 | "bytes", 360 | "fnv", 361 | "itoa", 362 | ] 363 | 364 | [[package]] 365 | name = "httparse" 366 | version = "1.3.4" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" 369 | 370 | [[package]] 371 | name = "humantime" 372 | version = "1.3.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 375 | dependencies = [ 376 | "quick-error", 377 | ] 378 | 379 | [[package]] 380 | name = "idna" 381 | version = "0.2.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 384 | dependencies = [ 385 | "matches", 386 | "unicode-bidi", 387 | "unicode-normalization", 388 | ] 389 | 390 | [[package]] 391 | name = "input_buffer" 392 | version = "0.3.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" 395 | dependencies = [ 396 | "bytes", 397 | ] 398 | 399 | [[package]] 400 | name = "itoa" 401 | version = "0.4.5" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 404 | 405 | [[package]] 406 | name = "lazy_static" 407 | version = "1.4.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 410 | 411 | [[package]] 412 | name = "libc" 413 | version = "0.2.68" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" 416 | 417 | [[package]] 418 | name = "log" 419 | version = "0.4.8" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 422 | dependencies = [ 423 | "cfg-if", 424 | ] 425 | 426 | [[package]] 427 | name = "matches" 428 | version = "0.1.8" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 431 | 432 | [[package]] 433 | name = "maybe-uninit" 434 | version = "2.0.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 437 | 438 | [[package]] 439 | name = "memchr" 440 | version = "2.3.3" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 443 | 444 | [[package]] 445 | name = "memoffset" 446 | version = "0.5.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" 449 | dependencies = [ 450 | "autocfg", 451 | ] 452 | 453 | [[package]] 454 | name = "nix" 455 | version = "0.17.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" 458 | dependencies = [ 459 | "bitflags", 460 | "cc", 461 | "cfg-if", 462 | "libc", 463 | "void", 464 | ] 465 | 466 | [[package]] 467 | name = "num_cpus" 468 | version = "1.13.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 471 | dependencies = [ 472 | "hermit-abi", 473 | "libc", 474 | ] 475 | 476 | [[package]] 477 | name = "once_cell" 478 | version = "1.3.1" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b" 481 | 482 | [[package]] 483 | name = "opaque-debug" 484 | version = "0.2.3" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 487 | 488 | [[package]] 489 | name = "percent-encoding" 490 | version = "2.1.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 493 | 494 | [[package]] 495 | name = "pin-project" 496 | version = "0.4.8" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" 499 | dependencies = [ 500 | "pin-project-internal", 501 | ] 502 | 503 | [[package]] 504 | name = "pin-project-internal" 505 | version = "0.4.8" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" 508 | dependencies = [ 509 | "proc-macro2", 510 | "quote", 511 | "syn", 512 | ] 513 | 514 | [[package]] 515 | name = "pin-utils" 516 | version = "0.1.0-alpha.4" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 519 | 520 | [[package]] 521 | name = "piper" 522 | version = "0.1.1" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "c6d62a6ea407d82215154475927b288219b79c8670e3371166210328e758ebaa" 525 | dependencies = [ 526 | "crossbeam-utils", 527 | "futures", 528 | ] 529 | 530 | [[package]] 531 | name = "pont-common" 532 | version = "0.1.0" 533 | dependencies = [ 534 | "rand", 535 | "serde", 536 | "serde_derive", 537 | ] 538 | 539 | [[package]] 540 | name = "pont-server" 541 | version = "0.1.0" 542 | dependencies = [ 543 | "anyhow", 544 | "async-tungstenite", 545 | "bincode", 546 | "env_logger", 547 | "futures", 548 | "lazy_static", 549 | "log", 550 | "num_cpus", 551 | "pont-common", 552 | "rand", 553 | "smol", 554 | "tungstenite", 555 | ] 556 | 557 | [[package]] 558 | name = "ppv-lite86" 559 | version = "0.2.6" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 562 | 563 | [[package]] 564 | name = "proc-macro-hack" 565 | version = "0.5.15" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" 568 | 569 | [[package]] 570 | name = "proc-macro-nested" 571 | version = "0.1.4" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" 574 | 575 | [[package]] 576 | name = "proc-macro2" 577 | version = "1.0.10" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 580 | dependencies = [ 581 | "unicode-xid", 582 | ] 583 | 584 | [[package]] 585 | name = "quick-error" 586 | version = "1.2.3" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 589 | 590 | [[package]] 591 | name = "quote" 592 | version = "1.0.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 595 | dependencies = [ 596 | "proc-macro2", 597 | ] 598 | 599 | [[package]] 600 | name = "rand" 601 | version = "0.7.3" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 604 | dependencies = [ 605 | "getrandom", 606 | "libc", 607 | "rand_chacha", 608 | "rand_core", 609 | "rand_hc", 610 | ] 611 | 612 | [[package]] 613 | name = "rand_chacha" 614 | version = "0.2.2" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 617 | dependencies = [ 618 | "ppv-lite86", 619 | "rand_core", 620 | ] 621 | 622 | [[package]] 623 | name = "rand_core" 624 | version = "0.5.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 627 | dependencies = [ 628 | "getrandom", 629 | ] 630 | 631 | [[package]] 632 | name = "rand_hc" 633 | version = "0.2.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 636 | dependencies = [ 637 | "rand_core", 638 | ] 639 | 640 | [[package]] 641 | name = "redox_syscall" 642 | version = "0.1.56" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 645 | 646 | [[package]] 647 | name = "regex" 648 | version = "1.3.6" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" 651 | dependencies = [ 652 | "aho-corasick", 653 | "memchr", 654 | "regex-syntax", 655 | "thread_local", 656 | ] 657 | 658 | [[package]] 659 | name = "regex-syntax" 660 | version = "0.6.17" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" 663 | 664 | [[package]] 665 | name = "scoped-tls-hkt" 666 | version = "0.1.2" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63" 669 | 670 | [[package]] 671 | name = "scopeguard" 672 | version = "1.1.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 675 | 676 | [[package]] 677 | name = "serde" 678 | version = "1.0.106" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 681 | dependencies = [ 682 | "serde_derive", 683 | ] 684 | 685 | [[package]] 686 | name = "serde_derive" 687 | version = "1.0.106" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 690 | dependencies = [ 691 | "proc-macro2", 692 | "quote", 693 | "syn", 694 | ] 695 | 696 | [[package]] 697 | name = "sha-1" 698 | version = "0.8.2" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" 701 | dependencies = [ 702 | "block-buffer", 703 | "digest", 704 | "fake-simd", 705 | "opaque-debug", 706 | ] 707 | 708 | [[package]] 709 | name = "slab" 710 | version = "0.4.2" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 713 | 714 | [[package]] 715 | name = "smallvec" 716 | version = "1.2.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 719 | 720 | [[package]] 721 | name = "smol" 722 | version = "0.1.4" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "f92bf48a84965d40061dbd596c17cfe0bff734b5aaf94d9408b0b3e382f7c06e" 725 | dependencies = [ 726 | "async-task", 727 | "crossbeam", 728 | "futures", 729 | "nix", 730 | "once_cell", 731 | "piper", 732 | "scoped-tls-hkt", 733 | "slab", 734 | "socket2", 735 | "wepoll-binding", 736 | ] 737 | 738 | [[package]] 739 | name = "socket2" 740 | version = "0.3.16" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "7fd8b795c389288baa5f355489c65e71fd48a02104600d15c4cfbc561e9e429d" 743 | dependencies = [ 744 | "cfg-if", 745 | "libc", 746 | "redox_syscall", 747 | "winapi", 748 | ] 749 | 750 | [[package]] 751 | name = "syn" 752 | version = "1.0.17" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 755 | dependencies = [ 756 | "proc-macro2", 757 | "quote", 758 | "unicode-xid", 759 | ] 760 | 761 | [[package]] 762 | name = "termcolor" 763 | version = "1.1.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 766 | dependencies = [ 767 | "winapi-util", 768 | ] 769 | 770 | [[package]] 771 | name = "thread_local" 772 | version = "1.0.1" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 775 | dependencies = [ 776 | "lazy_static", 777 | ] 778 | 779 | [[package]] 780 | name = "tungstenite" 781 | version = "0.10.1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "cfea31758bf674f990918962e8e5f07071a3161bd7c4138ed23e416e1ac4264e" 784 | dependencies = [ 785 | "base64", 786 | "byteorder", 787 | "bytes", 788 | "http", 789 | "httparse", 790 | "input_buffer", 791 | "log", 792 | "rand", 793 | "sha-1", 794 | "url", 795 | "utf-8", 796 | ] 797 | 798 | [[package]] 799 | name = "typenum" 800 | version = "1.11.2" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" 803 | 804 | [[package]] 805 | name = "unicode-bidi" 806 | version = "0.3.4" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 809 | dependencies = [ 810 | "matches", 811 | ] 812 | 813 | [[package]] 814 | name = "unicode-normalization" 815 | version = "0.1.12" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 818 | dependencies = [ 819 | "smallvec", 820 | ] 821 | 822 | [[package]] 823 | name = "unicode-xid" 824 | version = "0.2.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 827 | 828 | [[package]] 829 | name = "url" 830 | version = "2.1.1" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 833 | dependencies = [ 834 | "idna", 835 | "matches", 836 | "percent-encoding", 837 | ] 838 | 839 | [[package]] 840 | name = "utf-8" 841 | version = "0.7.5" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" 844 | 845 | [[package]] 846 | name = "void" 847 | version = "1.0.2" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 850 | 851 | [[package]] 852 | name = "wasi" 853 | version = "0.9.0+wasi-snapshot-preview1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 856 | 857 | [[package]] 858 | name = "wepoll-binding" 859 | version = "2.0.0" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "720cc52662740f7097c7bbc15f7906e4d3f6e64e5ec70e84eb4690b70ce1afc5" 862 | dependencies = [ 863 | "bitflags", 864 | "wepoll-sys", 865 | ] 866 | 867 | [[package]] 868 | name = "wepoll-sys" 869 | version = "2.0.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "9082a777aed991f6769e2b654aa0cb29f1c3d615daf009829b07b66c7aff6a24" 872 | dependencies = [ 873 | "cc", 874 | ] 875 | 876 | [[package]] 877 | name = "winapi" 878 | version = "0.3.8" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 881 | dependencies = [ 882 | "winapi-i686-pc-windows-gnu", 883 | "winapi-x86_64-pc-windows-gnu", 884 | ] 885 | 886 | [[package]] 887 | name = "winapi-i686-pc-windows-gnu" 888 | version = "0.4.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 891 | 892 | [[package]] 893 | name = "winapi-util" 894 | version = "0.1.4" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e" 897 | dependencies = [ 898 | "winapi", 899 | ] 900 | 901 | [[package]] 902 | name = "winapi-x86_64-pc-windows-gnu" 903 | version = "0.4.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 906 | -------------------------------------------------------------------------------- /pont-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pont-server" 3 | version = "0.1.0" 4 | authors = ["Matt Keeter "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [dependencies] 9 | async-tungstenite = "*" 10 | futures = "0.3.4" 11 | pont-common = { path = "../pont-common" } 12 | bincode = "1.3.1" 13 | rand = "^0.7" 14 | env_logger = "0.7.1" 15 | num_cpus = "1.13.0" 16 | log = "*" 17 | lazy_static = "*" 18 | smol = "*" 19 | anyhow = "*" 20 | 21 | [dependencies.tungstenite] 22 | version = "*" 23 | default-features = false 24 | -------------------------------------------------------------------------------- /pont-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use env_logger::Env; 2 | use log::{debug, error, info, trace, warn}; 3 | use rand::Rng; 4 | use std::{ 5 | collections::HashMap, 6 | env, 7 | io::Error as IoError, 8 | net::{SocketAddr, TcpListener, TcpStream}, 9 | sync::{Arc, Mutex}, 10 | time::Duration, 11 | }; 12 | 13 | use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; 14 | use futures::sink::SinkExt; 15 | use futures::stream::StreamExt; 16 | use futures::{future, future::join}; 17 | 18 | use anyhow::Result; 19 | use async_tungstenite::WebSocketStream; 20 | use smol::{Async, Task, Timer}; 21 | use tungstenite::Message as WebsocketMessage; 22 | 23 | use pont_common::{ClientMessage, Game, Piece, ServerMessage}; 24 | 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | lazy_static::lazy_static! { 28 | // words.txt is the EFF's random word list for passphrases 29 | static ref WORD_LIST: Vec<&'static str> = include_str!("words.txt") 30 | .split('\n') 31 | .filter(|w| !w.is_empty()) 32 | .collect(); 33 | } 34 | 35 | // Normally, a room exists as a relatively standalone task: 36 | // Client websockets send their messages to `write`, and `run_room` reads 37 | // them from `read` and applies them to the `room` object. 38 | // 39 | // It's made more complicated by the fact that adding players needs to 40 | // access the room object *before* clients are plugged into the `read`/`write` 41 | // infrastructure, so it must be shared and accessible from `handle_connection` 42 | type TaggedClientMessage = (SocketAddr, ClientMessage); 43 | #[derive(Clone)] 44 | struct RoomHandle { 45 | write: UnboundedSender, 46 | room: Arc>, 47 | } 48 | 49 | impl RoomHandle { 50 | async fn run_room(&mut self, mut read: UnboundedReceiver) { 51 | while let Some((addr, msg)) = read.next().await { 52 | if !self.room.lock().unwrap().on_message(addr, msg) { 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | 59 | async fn run_player( 60 | player_name: String, 61 | addr: SocketAddr, 62 | handle: RoomHandle, 63 | ws_stream: WebSocketStream>, 64 | ) { 65 | let (incoming, outgoing) = ws_stream.split(); 66 | 67 | // Messages to the player's websocket are mediated by a queue, 68 | // with a separate async task reading messages from the queue 69 | // and pushing them down the websocket. This lets us send messages to 70 | // a player without blocking or needing an extra await in the 71 | // main game loop, which would get awkward. 72 | let (ws_tx, ws_rx) = unbounded(); 73 | 74 | { 75 | // Briefly lock the room to add the player 76 | let room = &mut handle.room.lock().unwrap(); 77 | if let Err(e) = room.add_player(addr, player_name.clone(), ws_tx) { 78 | error!("[{}] Failed to add player: {:?}", room.name, e); 79 | return; 80 | } 81 | } 82 | 83 | let write = handle.write.clone(); 84 | let ra = ws_rx 85 | .map(|c| bincode::serialize(&c).unwrap_or_else(|_| panic!("Could not encode {:?}", c))) 86 | .map(WebsocketMessage::Binary) 87 | .map(Ok) 88 | .forward(incoming); 89 | 90 | // Match the config for bincode::deserialize, plus 1M size limit 91 | use bincode::Options; 92 | let config = bincode::config::DefaultOptions::new() 93 | .with_fixint_encoding() 94 | .allow_trailing_bytes() 95 | .with_limit(1024 * 1024); 96 | let rb = outgoing 97 | .map(|m| match m { 98 | Ok(WebsocketMessage::Binary(t)) => config.deserialize::(&t).ok(), 99 | _ => None, 100 | }) 101 | .take_while(|m| future::ready(m.is_some())) 102 | .map(|m| m.unwrap()) 103 | .chain(futures::stream::once(async { ClientMessage::Disconnected })) 104 | .map(move |m| Ok((addr, m))) 105 | .forward(write); 106 | let (ra, rb) = join(ra, rb).await; 107 | 108 | if let Err(e) = ra { 109 | error!( 110 | "[{}] Got error {} from player {}'s rx queue", 111 | addr, e, player_name 112 | ); 113 | } 114 | if let Err(e) = rb { 115 | error!( 116 | "[{}] Got error {} from player {}'s tx queue", 117 | addr, e, player_name 118 | ); 119 | } 120 | info!("[{}] Finished session with {}", addr, player_name); 121 | } 122 | 123 | type RoomList = Arc>>; 124 | 125 | #[derive(Default)] 126 | struct Room { 127 | name: String, 128 | started: bool, 129 | ended: bool, 130 | connections: HashMap, 131 | players: Vec, 132 | active_player: usize, 133 | game: Game, 134 | } 135 | 136 | struct Player { 137 | name: String, 138 | score: u32, 139 | hand: HashMap, 140 | ws: Option>, 141 | } 142 | 143 | impl Player { 144 | // Tries to remove a set of pieces from the player's hand 145 | // On failure, returns false. 146 | fn try_remove(&mut self, pieces: &[Piece]) -> bool { 147 | let mut count = HashMap::new(); 148 | for piece in pieces { 149 | *count.entry(piece).or_insert(0) += 1; 150 | } 151 | 152 | for (piece, n) in count.iter() { 153 | if let Some(m) = self.hand.get(&piece) { 154 | if *m < *n { 155 | return false; 156 | } 157 | } else { 158 | return false; 159 | } 160 | } 161 | 162 | for (piece, n) in count.iter() { 163 | if let Some(m) = self.hand.get_mut(&piece) { 164 | *m -= n; 165 | } 166 | } 167 | true 168 | } 169 | 170 | fn hand_is_empty(&self) -> bool { 171 | self.hand.values().all(|i| *i == 0) 172 | } 173 | 174 | fn hand_size(&self) -> usize { 175 | self.hand.values().sum::() 176 | } 177 | } 178 | 179 | impl Room { 180 | fn running(&self) -> bool { 181 | !self.connections.is_empty() 182 | } 183 | 184 | fn broadcast(&self, s: ServerMessage) { 185 | for c in self.connections.values() { 186 | if let Some(ws) = &self.players[*c].ws { 187 | if let Err(e) = ws.unbounded_send(s.clone()) { 188 | error!( 189 | "[{}] Failed to send broadcast to {}: {}", 190 | self.name, self.players[*c].name, e 191 | ); 192 | } 193 | } 194 | } 195 | } 196 | 197 | fn broadcast_except(&self, i: usize, s: ServerMessage) { 198 | for (j, p) in self.players.iter().enumerate() { 199 | if i != j { 200 | if let Some(ws) = p.ws.as_ref() { 201 | if let Err(e) = ws.unbounded_send(s.clone()) { 202 | error!( 203 | "[{}] Failed to send message to {}: {}", 204 | self.name, self.players[j].name, e 205 | ); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | fn send(&self, i: usize, s: ServerMessage) { 213 | if let Some(p) = self.players[i].ws.as_ref() { 214 | if let Err(e) = p.unbounded_send(s) { 215 | error!( 216 | "[{}] Failed to send message to {}: {}", 217 | self.name, self.players[i].name, e 218 | ); 219 | } 220 | } else { 221 | error!("[{}] Tried sending message to inactive player", self.name); 222 | } 223 | } 224 | 225 | fn add_player( 226 | &mut self, 227 | addr: SocketAddr, 228 | player_name: String, 229 | ws_tx: UnboundedSender, 230 | ) -> Result<()> { 231 | // Pick out a hand for our new player 232 | let hand = self.game.deal(6); 233 | let mut pieces = Vec::new(); 234 | for (piece, count) in hand.iter() { 235 | for _i in 0..*count { 236 | pieces.push(piece.clone()); 237 | } 238 | } 239 | 240 | // Check whether the new player's name matches an old name of someone 241 | // that has disconnected. If so, we can take their seat. 242 | let mut player_index = None; 243 | for (i, p) in self.players.iter().enumerate() { 244 | if p.name == player_name && p.ws.is_none() { 245 | player_index = Some(i); 246 | break; 247 | } 248 | } 249 | 250 | if let Some(i) = player_index { 251 | // Reclaim the player's spot 252 | self.broadcast(ServerMessage::PlayerReconnected(i)); 253 | self.players[i].hand = hand; 254 | self.players[i].ws = Some(ws_tx.clone()); 255 | } else { 256 | self.broadcast(ServerMessage::NewPlayer(player_name.clone())); 257 | player_index = Some(self.players.len()); 258 | 259 | self.players.push(Player { 260 | name: player_name, 261 | score: 0, 262 | hand, 263 | ws: Some(ws_tx.clone()), 264 | }); 265 | } 266 | 267 | // At this point, the option must be assigned, so we unwrap it 268 | let player_index = player_index.unwrap(); 269 | 270 | // Add the new player to the active list of connections and players 271 | self.connections.insert(addr, player_index); 272 | 273 | // The game counts as started once the first player joins 274 | self.started = true; 275 | 276 | // Tell the player that they have joined the room 277 | ws_tx.unbounded_send(ServerMessage::JoinedRoom { 278 | room_name: self.name.clone(), 279 | players: self 280 | .players 281 | .iter() 282 | .map(|p| (p.name.clone(), p.score, p.ws.is_some())) 283 | .collect(), 284 | active_player: self.active_player, 285 | player_index, 286 | board: self.game.board.iter().map(|(k, v)| (*k, *v)).collect(), 287 | pieces, 288 | })?; 289 | 290 | // Because we've removed pieces from the bag, update the 291 | // pieces remaining that clients know about. 292 | self.broadcast(ServerMessage::PiecesRemaining(self.game.bag.len())); 293 | Ok(()) 294 | } 295 | 296 | fn next_player(&mut self) { 297 | if !self.connections.is_empty() { 298 | self.active_player = (self.active_player + 1) % self.players.len(); 299 | while self.players[self.active_player].ws.is_none() { 300 | self.active_player = (self.active_player + 1) % self.players.len(); 301 | } 302 | debug!( 303 | "[{}] Active player changed to {}", 304 | self.name, self.players[self.active_player].name 305 | ); 306 | 307 | self.broadcast(ServerMessage::PlayerTurn(self.active_player)); 308 | } 309 | } 310 | 311 | fn on_client_disconnected(&mut self, addr: SocketAddr) { 312 | if let Some(p) = self.connections.remove(&addr) { 313 | let player_name = self.players[p].name.clone(); 314 | info!( 315 | "[{}] Removed disconnected player '{}'", 316 | self.name, player_name 317 | ); 318 | self.players[p].ws = None; 319 | for (k, v) in self.players[p].hand.drain() { 320 | for _i in 0..v { 321 | self.game.bag.push(k.clone()); 322 | } 323 | } 324 | self.game.shuffle(); 325 | self.broadcast(ServerMessage::PlayerDisconnected(p)); 326 | 327 | // We've put pieces back in the bag, so update the piece count 328 | self.broadcast(ServerMessage::PiecesRemaining(self.game.bag.len())); 329 | 330 | // Find the next active player and broadcast out that info 331 | if p == self.active_player { 332 | self.next_player(); 333 | } 334 | } else { 335 | error!( 336 | "[{}] Tried to remove non-existent player at {}", 337 | self.name, addr 338 | ); 339 | } 340 | } 341 | 342 | fn on_play(&mut self, pieces: &[(Piece, i32, i32)]) { 343 | let player = &mut self.players[self.active_player]; 344 | 345 | let mut board = self.game.board.clone(); 346 | for (piece, x, y) in pieces.iter() { 347 | board.insert((*x, *y), *piece); 348 | } 349 | let played = pieces.iter().map(|p| (p.1, p.2)).collect::>(); 350 | if !Game::invalid(&board).is_empty() || !Game::is_linear_connected(&board, &played) { 351 | warn!( 352 | "[{}] Player {} tried to make an illegal move", 353 | self.name, player.name 354 | ); 355 | self.send(self.active_player, ServerMessage::MoveRejected); 356 | return; 357 | } 358 | 359 | { 360 | // Remove the pieces from the player's hand 361 | let pieces: Vec = pieces.iter().map(|p| p.0).collect(); 362 | if !player.try_remove(&pieces) { 363 | warn!( 364 | "[{}] Player {} tried to play an unowned piece", 365 | self.name, player.name 366 | ); 367 | self.send(self.active_player, ServerMessage::MoveRejected); 368 | return; 369 | } 370 | } 371 | 372 | if let Some(mut delta) = self.game.play(pieces) { 373 | // Broadcast the new score to all players 374 | let mut deal = Vec::new(); 375 | for (piece, count) in self.game.deal(6 - player.hand_size()) { 376 | *player.hand.entry(piece).or_insert(0) += count; 377 | for _i in 0..count { 378 | deal.push(piece); 379 | } 380 | } 381 | // Check whether the game is over! 382 | let over = player.hand_is_empty() && self.game.bag.is_empty(); 383 | if over { 384 | delta += 6; 385 | } 386 | player.score += delta; 387 | 388 | let total = player.score; // Release the borrow of player 389 | self.broadcast(ServerMessage::PlayerScore { delta, total }); 390 | self.broadcast(ServerMessage::PiecesRemaining(self.game.bag.len())); 391 | self.send(self.active_player, ServerMessage::MoveAccepted(deal)); 392 | 393 | // Broadcast the play to other players 394 | self.broadcast_except(self.active_player, ServerMessage::Played(pieces.to_vec())); 395 | 396 | if over { 397 | let winner = self 398 | .players 399 | .iter() 400 | .enumerate() 401 | .max_by_key(|(_i, p)| p.score) 402 | .unwrap() 403 | .0; 404 | self.broadcast(ServerMessage::ItsOver(winner)); 405 | self.ended = true; 406 | } 407 | } else { 408 | warn!( 409 | "[{}] Player {} snuck an illegal move past the first filters", 410 | self.name, player.name 411 | ); 412 | self.send(self.active_player, ServerMessage::MoveRejected); 413 | } 414 | } 415 | 416 | fn on_swap(&mut self, pieces: &[Piece]) { 417 | let player = &mut self.players[self.active_player]; 418 | if !player.try_remove(pieces) { 419 | warn!( 420 | "[{}] Player {} tried to play an unowned piece", 421 | self.name, player.name 422 | ); 423 | self.send(self.active_player, ServerMessage::MoveRejected); 424 | } else if let Some(deal) = self.game.swap(pieces) { 425 | for piece in deal.iter() { 426 | *player.hand.entry(*piece).or_insert(0) += 1; 427 | } 428 | self.send(self.active_player, ServerMessage::MoveAccepted(deal)); 429 | 430 | // Broadcast the swap to other players 431 | // This doesn't change piece count, so we don't need to broadcast 432 | // PiecesRemaining to the players. 433 | self.broadcast(ServerMessage::Swapped(pieces.len())); 434 | } else { 435 | warn!( 436 | "[{}] Player {} couldn't be dealt {} pieces", 437 | self.name, 438 | player.name, 439 | pieces.len() 440 | ); 441 | } 442 | } 443 | 444 | fn on_message(&mut self, addr: SocketAddr, msg: ClientMessage) -> bool { 445 | trace!( 446 | "[{}] Got message {:?} from {}", 447 | self.name, 448 | msg, 449 | self.connections 450 | .get(&addr) 451 | .map(|i| self.players[*i].name.clone()) 452 | .unwrap_or_else(|| format!("unknown player at {}", addr)) 453 | ); 454 | match msg { 455 | ClientMessage::Disconnected => self.on_client_disconnected(addr), 456 | ClientMessage::Chat(c) => { 457 | let name = self 458 | .connections 459 | .get(&addr) 460 | .map_or("unknown", |i| &self.players[*i].name); 461 | self.broadcast(ServerMessage::Chat { 462 | from: name.to_string(), 463 | message: c, 464 | }); 465 | } 466 | ClientMessage::CreateRoom(_) | ClientMessage::JoinRoom(_, _) => { 467 | warn!("[{}] Invalid client message {:?}", self.name, msg); 468 | } 469 | ClientMessage::Play(pieces) => { 470 | if self.ended { 471 | warn!("[{}] Got play after move ended", self.name); 472 | } else if let Some(i) = self.connections.get(&addr).copied() { 473 | if i == self.active_player { 474 | self.on_play(&pieces); 475 | if !self.ended { 476 | self.next_player(); 477 | } 478 | } else { 479 | warn!("[{}] Player {} out of turn", self.name, addr); 480 | } 481 | } else { 482 | warn!("[{}] Invalid player {}", self.name, addr); 483 | } 484 | } 485 | ClientMessage::Swap(pieces) => { 486 | if self.ended { 487 | warn!("[{}] Got play after move ended", self.name); 488 | } else if let Some(i) = self.connections.get(&addr).copied() { 489 | if i == self.active_player { 490 | self.on_swap(&pieces); 491 | self.next_player(); 492 | } else { 493 | warn!("[{}] Player {} out of turn", self.name, addr); 494 | } 495 | } else { 496 | warn!("[{}] Invalid player {}", self.name, addr); 497 | } 498 | } 499 | } 500 | self.running() 501 | } 502 | } 503 | 504 | fn next_room_name(rooms: &mut HashMap, handle: RoomHandle) -> String { 505 | // This loop should only run once, unless we're starting to saturate the 506 | // space of possible room names (which is quite large) 507 | let mut rng = rand::thread_rng(); 508 | loop { 509 | let room_name = format!( 510 | "{} {} {}", 511 | WORD_LIST[rng.gen_range(0, WORD_LIST.len())], 512 | WORD_LIST[rng.gen_range(0, WORD_LIST.len())], 513 | WORD_LIST[rng.gen_range(0, WORD_LIST.len())] 514 | ); 515 | use std::collections::hash_map::Entry; 516 | if let Entry::Vacant(v) = rooms.entry(room_name.clone()) { 517 | v.insert(handle); 518 | return room_name; 519 | } 520 | } 521 | } 522 | 523 | async fn handle_connection( 524 | rooms: RoomList, 525 | raw_stream: Async, 526 | addr: SocketAddr, 527 | mut close_room: UnboundedSender, 528 | ) -> Result<()> { 529 | info!("[{}] Incoming TCP connection", addr); 530 | 531 | let mut ws_stream = async_tungstenite::accept_async(raw_stream).await?; 532 | info!("[{}] WebSocket connection established", addr); 533 | 534 | // Clients are only allowed to send text messages at this stage. 535 | // If they do anything else, then just disconnect. 536 | while let Some(Ok(WebsocketMessage::Binary(t))) = ws_stream.next().await { 537 | let msg = bincode::deserialize::(&t)?; 538 | 539 | // Try to interpret their message as joining a room 540 | match msg { 541 | ClientMessage::CreateRoom(player_name) => { 542 | // Log to link address and player name 543 | 544 | // We'll funnel all Websocket communication through one 545 | // MPSC queue per room, with websockets running in their 546 | // own little tasks writing to the queue. 547 | let (write, read) = unbounded(); 548 | 549 | let room = Arc::new(Mutex::new(Room::default())); 550 | let handle = RoomHandle { write, room }; 551 | // Lock the global room list for a short time 552 | let room_name = { 553 | let map = &mut rooms.lock().unwrap(); 554 | next_room_name(map, handle.clone()) 555 | }; 556 | info!( 557 | "[{}] Creating room '{}' for player {}", 558 | addr, room_name, player_name 559 | ); 560 | handle.room.lock().unwrap().name = room_name.clone(); 561 | 562 | // To avoid spawning a new task, we'll use this task to run 563 | // both the player's tx/rx queues *and* the room itself. 564 | let mut h = handle.clone(); 565 | join( 566 | h.run_room(read), 567 | run_player(player_name, addr, handle, ws_stream), 568 | ) 569 | .await; 570 | 571 | info!("[{}] All players left, closing room.", room_name); 572 | if let Err(e) = close_room.send(room_name.clone()).await { 573 | error!("[{}] Failed to close room: {}", room_name, e); 574 | } 575 | 576 | return Ok(()); 577 | } 578 | ClientMessage::JoinRoom(name, room_name) => { 579 | // Log to link address and player name 580 | info!("[{}] Player {} sent JoinRoom({})", addr, name, room_name); 581 | 582 | // If the room name is valid, then join it by passing 583 | // the new user and their connection into the room task 584 | // 585 | // We do a little bit of dancing here to avoid the borrow 586 | // checker, since the tx in tx.send(...).await must live 587 | // through yield points. 588 | let handle = rooms.lock().unwrap().get_mut(&room_name).cloned(); 589 | 590 | // If we tried to join an existing room, then check that there 591 | // are enough pieces in the bag to deal a full hand. 592 | if let Some(h) = handle { 593 | if !h.room.lock().unwrap().game.bag.is_empty() { 594 | // Happy case: add the player to the room, then switch 595 | // to running the player's communication task 596 | run_player(name, addr, h, ws_stream).await; 597 | return Ok(()); 598 | } else { 599 | // Not enough pieces, so report an error to the client 600 | let msg = ServerMessage::JoinFailed("Not enough pieces left".to_string()); 601 | let encoded = bincode::serialize(&msg)?; 602 | ws_stream.send(WebsocketMessage::Binary(encoded)).await?; 603 | } 604 | } else { 605 | // Otherwise, reply that we don't know anything about that 606 | // particular room name. 607 | let msg = 608 | ServerMessage::JoinFailed(format!("Could not find room '{}'", room_name)); 609 | let encoded = bincode::serialize(&msg)?; 610 | ws_stream.send(WebsocketMessage::Binary(encoded)).await?; 611 | } 612 | } 613 | // If they send an illegal message, then they obviously have ill 614 | // intentions and we should disconnect them right now. 615 | msg => { 616 | warn!("[{}] Got unexpected message {:?}", addr, msg); 617 | break; 618 | } 619 | } 620 | } 621 | info!("[{}] Dropping connection", addr); 622 | Ok(()) 623 | } 624 | 625 | fn main() -> Result<(), IoError> { 626 | env_logger::from_env(Env::default().default_filter_or("pont_server=INFO")).init(); 627 | 628 | // Create an executor thread pool. 629 | for _ in 0..num_cpus::get().max(1) { 630 | std::thread::spawn(|| smol::run(future::pending::<()>())); 631 | } 632 | 633 | let rooms = RoomList::new(Mutex::new(HashMap::new())); 634 | 635 | // Run a small task whose job is to close rooms when the last player leaves. 636 | // This task accepts room names through a MPSC queue, which all of the 637 | // room tasks push their names into. 638 | let close_room = { 639 | let (tx, mut rx) = unbounded(); 640 | let rooms = rooms.clone(); 641 | Task::spawn(async move { 642 | while let Some(r) = rx.next().await { 643 | info!("Closing room [{}]", r); 644 | rooms.lock().unwrap().remove(&r); 645 | } 646 | }) 647 | .detach(); 648 | tx 649 | }; 650 | 651 | { 652 | // Periodically print the number of open rooms to the logs 653 | let rooms = rooms.clone(); 654 | Task::spawn(async move { 655 | let mut prev_count = 0; 656 | loop { 657 | Timer::after(Duration::from_secs(60)).await; 658 | let count = rooms.lock().unwrap().len(); 659 | if count != prev_count { 660 | info!("{} rooms open", count); 661 | prev_count = count; 662 | } 663 | } 664 | }) 665 | .detach() 666 | } 667 | 668 | // The target address + port is optionally specified on the command line 669 | let addr = env::args() 670 | .nth(1) 671 | .unwrap_or_else(|| "0.0.0.0:8080".to_string()); 672 | 673 | smol::block_on(async { 674 | // Create the event loop and TCP listener we'll accept connections on. 675 | info!("Listening on: {}", addr); 676 | let listener = Async::::bind(addr).expect("Could not create listener"); 677 | 678 | // The main loop accepts incoming connections asynchronously 679 | while let Ok((stream, addr)) = listener.accept().await { 680 | let close_room = close_room.clone(); 681 | let rooms = rooms.clone(); 682 | Task::spawn(async move { 683 | if let Err(e) = handle_connection(rooms, stream, addr, close_room).await { 684 | warn!("Failed to handle connection from {}: {}", addr, e); 685 | } 686 | }) 687 | .detach(); 688 | } 689 | }); 690 | 691 | Ok(()) 692 | } 693 | -------------------------------------------------------------------------------- /pont-server/src/words.txt: -------------------------------------------------------------------------------- 1 | acid 2 | acorn 3 | acre 4 | acts 5 | afar 6 | affix 7 | aged 8 | agent 9 | agile 10 | aging 11 | agony 12 | ahead 13 | aide 14 | aids 15 | aim 16 | ajar 17 | alarm 18 | alias 19 | alibi 20 | alien 21 | alike 22 | alive 23 | aloe 24 | aloft 25 | aloha 26 | alone 27 | amend 28 | amino 29 | ample 30 | amuse 31 | angel 32 | anger 33 | angle 34 | ankle 35 | apple 36 | april 37 | apron 38 | aqua 39 | area 40 | arena 41 | argue 42 | arise 43 | armed 44 | armor 45 | army 46 | aroma 47 | array 48 | arson 49 | art 50 | ashen 51 | ashes 52 | atlas 53 | atom 54 | attic 55 | audio 56 | avert 57 | avoid 58 | awake 59 | award 60 | awoke 61 | axis 62 | bacon 63 | badge 64 | bagel 65 | baggy 66 | baked 67 | baker 68 | balmy 69 | banjo 70 | barge 71 | barn 72 | bash 73 | basil 74 | bask 75 | batch 76 | bath 77 | baton 78 | bats 79 | blade 80 | blank 81 | blast 82 | blaze 83 | bleak 84 | blend 85 | bless 86 | blimp 87 | blink 88 | bloat 89 | blob 90 | blog 91 | blot 92 | blunt 93 | blurt 94 | blush 95 | boast 96 | boat 97 | body 98 | boil 99 | bok 100 | bolt 101 | boned 102 | boney 103 | bonus 104 | bony 105 | book 106 | booth 107 | boots 108 | boss 109 | botch 110 | both 111 | boxer 112 | breed 113 | bribe 114 | brick 115 | bride 116 | brim 117 | bring 118 | brink 119 | brisk 120 | broad 121 | broil 122 | broke 123 | brook 124 | broom 125 | brush 126 | buck 127 | bud 128 | buggy 129 | bulge 130 | bulk 131 | bully 132 | bunch 133 | bunny 134 | bunt 135 | bush 136 | bust 137 | busy 138 | buzz 139 | cable 140 | cache 141 | cadet 142 | cage 143 | cake 144 | calm 145 | cameo 146 | canal 147 | candy 148 | cane 149 | canon 150 | cape 151 | card 152 | cargo 153 | carol 154 | carry 155 | carve 156 | case 157 | cash 158 | cause 159 | cedar 160 | chain 161 | chair 162 | chant 163 | chaos 164 | charm 165 | chase 166 | cheek 167 | cheer 168 | chef 169 | chess 170 | chest 171 | chew 172 | chief 173 | chili 174 | chill 175 | chip 176 | chomp 177 | chop 178 | chow 179 | chuck 180 | chump 181 | chunk 182 | churn 183 | chute 184 | cider 185 | cinch 186 | city 187 | civic 188 | civil 189 | clad 190 | claim 191 | clamp 192 | clap 193 | clash 194 | clasp 195 | class 196 | claw 197 | clay 198 | clean 199 | clear 200 | cleat 201 | cleft 202 | clerk 203 | click 204 | cling 205 | clink 206 | clip 207 | cloak 208 | clock 209 | clone 210 | cloth 211 | cloud 212 | clump 213 | coach 214 | coast 215 | coat 216 | cod 217 | coil 218 | coke 219 | cola 220 | cold 221 | colt 222 | coma 223 | come 224 | comic 225 | comma 226 | cone 227 | cope 228 | copy 229 | coral 230 | cork 231 | cost 232 | cot 233 | couch 234 | cough 235 | cover 236 | cozy 237 | craft 238 | cramp 239 | crane 240 | crank 241 | crate 242 | crave 243 | crawl 244 | crazy 245 | creme 246 | crepe 247 | crept 248 | crib 249 | cried 250 | crisp 251 | crook 252 | crop 253 | cross 254 | crowd 255 | crown 256 | crumb 257 | crush 258 | crust 259 | cub 260 | cult 261 | cupid 262 | cure 263 | curl 264 | curry 265 | curse 266 | curve 267 | curvy 268 | cushy 269 | cut 270 | cycle 271 | dab 272 | dad 273 | daily 274 | dairy 275 | daisy 276 | dance 277 | dandy 278 | darn 279 | dart 280 | dash 281 | data 282 | date 283 | dawn 284 | deaf 285 | deal 286 | dean 287 | debit 288 | debt 289 | debug 290 | decaf 291 | decal 292 | decay 293 | deck 294 | decor 295 | decoy 296 | deed 297 | delay 298 | denim 299 | dense 300 | dent 301 | depth 302 | derby 303 | desk 304 | dial 305 | diary 306 | dice 307 | dig 308 | dill 309 | dime 310 | dimly 311 | diner 312 | dingy 313 | disco 314 | dish 315 | disk 316 | ditch 317 | ditzy 318 | dizzy 319 | dock 320 | dodge 321 | doing 322 | doll 323 | dome 324 | donor 325 | donut 326 | dose 327 | dot 328 | dove 329 | down 330 | dowry 331 | doze 332 | drab 333 | drama 334 | drank 335 | draw 336 | dress 337 | dried 338 | drift 339 | drill 340 | drive 341 | drone 342 | droop 343 | drove 344 | drown 345 | drum 346 | dry 347 | duck 348 | duct 349 | dude 350 | dug 351 | duke 352 | duo 353 | dusk 354 | dust 355 | duty 356 | dwarf 357 | dwell 358 | eagle 359 | early 360 | earth 361 | easel 362 | east 363 | eaten 364 | eats 365 | ebay 366 | ebony 367 | ebook 368 | echo 369 | edge 370 | eel 371 | eject 372 | elbow 373 | elder 374 | elf 375 | elk 376 | elm 377 | elope 378 | elude 379 | elves 380 | email 381 | emit 382 | empty 383 | emu 384 | enter 385 | entry 386 | envoy 387 | equal 388 | erase 389 | error 390 | erupt 391 | essay 392 | etch 393 | evade 394 | even 395 | evict 396 | evil 397 | evoke 398 | exact 399 | exit 400 | fable 401 | faced 402 | fact 403 | fade 404 | fall 405 | false 406 | fancy 407 | fang 408 | fax 409 | feast 410 | feed 411 | femur 412 | fence 413 | fend 414 | ferry 415 | fetal 416 | fetch 417 | fever 418 | fiber 419 | fifth 420 | fifty 421 | film 422 | filth 423 | final 424 | finch 425 | fit 426 | five 427 | flag 428 | flaky 429 | flame 430 | flap 431 | flask 432 | fled 433 | flick 434 | fling 435 | flint 436 | flip 437 | flirt 438 | float 439 | flock 440 | flop 441 | floss 442 | flyer 443 | foam 444 | foe 445 | fog 446 | foil 447 | folic 448 | folk 449 | food 450 | fool 451 | found 452 | fox 453 | foyer 454 | frail 455 | frame 456 | fray 457 | fresh 458 | fried 459 | frill 460 | frisk 461 | from 462 | front 463 | frost 464 | froth 465 | frown 466 | froze 467 | fruit 468 | gag 469 | gains 470 | gala 471 | game 472 | gap 473 | gas 474 | gave 475 | gear 476 | gecko 477 | geek 478 | gem 479 | genre 480 | gift 481 | gig 482 | gills 483 | given 484 | giver 485 | glad 486 | glass 487 | glide 488 | gloss 489 | glove 490 | glow 491 | glue 492 | goal 493 | going 494 | golf 495 | gong 496 | good 497 | gooey 498 | goofy 499 | gore 500 | gown 501 | grab 502 | grain 503 | grant 504 | grape 505 | graph 506 | grasp 507 | grass 508 | grave 509 | gravy 510 | gray 511 | green 512 | greet 513 | grew 514 | grid 515 | grief 516 | grill 517 | grip 518 | grit 519 | groom 520 | grope 521 | growl 522 | grub 523 | grunt 524 | guide 525 | gulf 526 | gulp 527 | gummy 528 | guru 529 | gush 530 | gut 531 | guy 532 | habit 533 | half 534 | halo 535 | halt 536 | happy 537 | harm 538 | hash 539 | hasty 540 | hatch 541 | hate 542 | haven 543 | hazel 544 | hazy 545 | heap 546 | heat 547 | heave 548 | hedge 549 | hefty 550 | help 551 | herbs 552 | hers 553 | hub 554 | hug 555 | hula 556 | hull 557 | human 558 | humid 559 | hump 560 | hung 561 | hunk 562 | hunt 563 | hurry 564 | hurt 565 | hush 566 | hut 567 | ice 568 | icing 569 | icon 570 | icy 571 | igloo 572 | image 573 | ion 574 | iron 575 | islam 576 | issue 577 | item 578 | ivory 579 | ivy 580 | jab 581 | jam 582 | jaws 583 | jazz 584 | jeep 585 | jelly 586 | jet 587 | jiffy 588 | job 589 | jog 590 | jolly 591 | jolt 592 | jot 593 | joy 594 | judge 595 | juice 596 | juicy 597 | july 598 | jumbo 599 | jump 600 | junky 601 | juror 602 | jury 603 | keep 604 | keg 605 | kept 606 | kick 607 | kilt 608 | king 609 | kite 610 | kitty 611 | kiwi 612 | knee 613 | knelt 614 | koala 615 | kung 616 | ladle 617 | lady 618 | lair 619 | lake 620 | lance 621 | land 622 | lapel 623 | large 624 | lash 625 | lasso 626 | last 627 | latch 628 | late 629 | lazy 630 | left 631 | legal 632 | lemon 633 | lend 634 | lens 635 | lent 636 | level 637 | lever 638 | lid 639 | life 640 | lift 641 | lilac 642 | lily 643 | limb 644 | limes 645 | line 646 | lint 647 | lion 648 | lip 649 | list 650 | lived 651 | liver 652 | lunar 653 | lunch 654 | lung 655 | lurch 656 | lure 657 | lurk 658 | lying 659 | lyric 660 | mace 661 | maker 662 | malt 663 | mama 664 | mango 665 | manor 666 | many 667 | map 668 | march 669 | mardi 670 | marry 671 | mash 672 | match 673 | mate 674 | math 675 | moan 676 | mocha 677 | moist 678 | mold 679 | mom 680 | moody 681 | mop 682 | morse 683 | most 684 | motor 685 | motto 686 | mount 687 | mouse 688 | mousy 689 | mouth 690 | move 691 | movie 692 | mower 693 | mud 694 | mug 695 | mulch 696 | mule 697 | mull 698 | mumbo 699 | mummy 700 | mural 701 | muse 702 | music 703 | musky 704 | mute 705 | nacho 706 | nag 707 | nail 708 | name 709 | nanny 710 | nap 711 | navy 712 | near 713 | neat 714 | neon 715 | nerd 716 | nest 717 | net 718 | next 719 | niece 720 | ninth 721 | nutty 722 | oak 723 | oasis 724 | oat 725 | ocean 726 | oil 727 | old 728 | olive 729 | omen 730 | onion 731 | only 732 | ooze 733 | opal 734 | open 735 | opera 736 | opt 737 | otter 738 | ouch 739 | ounce 740 | outer 741 | oval 742 | oven 743 | owl 744 | ozone 745 | pace 746 | pagan 747 | pager 748 | palm 749 | panda 750 | panic 751 | pants 752 | panty 753 | paper 754 | park 755 | party 756 | pasta 757 | patch 758 | path 759 | patio 760 | payer 761 | pecan 762 | penny 763 | pep 764 | perch 765 | perky 766 | perm 767 | pest 768 | petal 769 | petri 770 | petty 771 | photo 772 | plank 773 | plant 774 | plaza 775 | plead 776 | plot 777 | plow 778 | pluck 779 | plug 780 | plus 781 | poach 782 | pod 783 | poem 784 | poet 785 | pogo 786 | point 787 | poise 788 | poker 789 | polar 790 | polio 791 | polka 792 | polo 793 | pond 794 | pony 795 | poppy 796 | pork 797 | poser 798 | pouch 799 | pound 800 | pout 801 | power 802 | prank 803 | press 804 | print 805 | prior 806 | prism 807 | prize 808 | probe 809 | prong 810 | proof 811 | props 812 | prude 813 | prune 814 | pry 815 | pug 816 | pull 817 | pulp 818 | pulse 819 | puma 820 | punch 821 | punk 822 | pupil 823 | puppy 824 | purr 825 | purse 826 | push 827 | putt 828 | quack 829 | quake 830 | query 831 | quiet 832 | quill 833 | quilt 834 | quit 835 | quota 836 | quote 837 | rabid 838 | race 839 | rack 840 | radar 841 | radio 842 | raft 843 | rage 844 | raid 845 | rail 846 | rake 847 | rally 848 | ramp 849 | ranch 850 | range 851 | rank 852 | rant 853 | rash 854 | raven 855 | reach 856 | react 857 | ream 858 | rebel 859 | recap 860 | relax 861 | relay 862 | relic 863 | remix 864 | repay 865 | repel 866 | reply 867 | rerun 868 | reset 869 | rhyme 870 | rice 871 | rich 872 | ride 873 | rigid 874 | rigor 875 | rinse 876 | riot 877 | ripen 878 | rise 879 | risk 880 | ritzy 881 | rival 882 | river 883 | roast 884 | robe 885 | robin 886 | rock 887 | rogue 888 | roman 889 | romp 890 | rope 891 | rover 892 | royal 893 | ruby 894 | rug 895 | ruin 896 | rule 897 | runny 898 | rush 899 | rust 900 | rut 901 | sadly 902 | sage 903 | said 904 | saint 905 | salad 906 | salon 907 | salsa 908 | salt 909 | same 910 | sandy 911 | santa 912 | satin 913 | sauna 914 | saved 915 | savor 916 | sax 917 | say 918 | scale 919 | scam 920 | scan 921 | scare 922 | scarf 923 | scary 924 | scoff 925 | scold 926 | scoop 927 | scoot 928 | scope 929 | score 930 | scorn 931 | scout 932 | scowl 933 | scrap 934 | scrub 935 | scuba 936 | scuff 937 | sect 938 | sedan 939 | self 940 | send 941 | sepia 942 | serve 943 | set 944 | seven 945 | shack 946 | shade 947 | shady 948 | shaft 949 | shaky 950 | sham 951 | shape 952 | share 953 | sharp 954 | shed 955 | sheep 956 | sheet 957 | shelf 958 | shell 959 | shine 960 | shiny 961 | ship 962 | shirt 963 | shock 964 | shop 965 | shore 966 | shout 967 | shove 968 | shown 969 | showy 970 | shred 971 | shrug 972 | shun 973 | shush 974 | shut 975 | shy 976 | sift 977 | silk 978 | silly 979 | silo 980 | sip 981 | siren 982 | sixth 983 | size 984 | skate 985 | skew 986 | skid 987 | skier 988 | skies 989 | skip 990 | skirt 991 | skit 992 | sky 993 | slab 994 | slack 995 | slain 996 | slam 997 | slang 998 | slash 999 | slate 1000 | slaw 1001 | sled 1002 | sleek 1003 | sleep 1004 | sleet 1005 | slept 1006 | slice 1007 | slick 1008 | slimy 1009 | sling 1010 | slip 1011 | slit 1012 | slob 1013 | slot 1014 | slug 1015 | slum 1016 | slurp 1017 | slush 1018 | small 1019 | smash 1020 | smell 1021 | smile 1022 | smirk 1023 | smog 1024 | snack 1025 | snap 1026 | snare 1027 | snarl 1028 | sneak 1029 | sneer 1030 | sniff 1031 | snore 1032 | snort 1033 | snout 1034 | snowy 1035 | snub 1036 | snuff 1037 | speak 1038 | speed 1039 | spend 1040 | spent 1041 | spew 1042 | spied 1043 | spill 1044 | spiny 1045 | spoil 1046 | spoke 1047 | spoof 1048 | spool 1049 | spoon 1050 | sport 1051 | spot 1052 | spout 1053 | spray 1054 | spree 1055 | spur 1056 | squad 1057 | squat 1058 | squid 1059 | stack 1060 | staff 1061 | stage 1062 | stain 1063 | stall 1064 | stamp 1065 | stand 1066 | stank 1067 | stark 1068 | start 1069 | stash 1070 | state 1071 | stays 1072 | steam 1073 | steep 1074 | stem 1075 | step 1076 | stew 1077 | stick 1078 | sting 1079 | stir 1080 | stock 1081 | stole 1082 | stomp 1083 | stony 1084 | stood 1085 | stool 1086 | stoop 1087 | stop 1088 | storm 1089 | stout 1090 | stove 1091 | straw 1092 | stray 1093 | strut 1094 | stuck 1095 | stud 1096 | stuff 1097 | stump 1098 | stung 1099 | stunt 1100 | suds 1101 | sugar 1102 | sulk 1103 | surf 1104 | sushi 1105 | swab 1106 | swan 1107 | swarm 1108 | sway 1109 | swear 1110 | sweat 1111 | sweep 1112 | swell 1113 | swept 1114 | swim 1115 | swing 1116 | swipe 1117 | swirl 1118 | swoop 1119 | swore 1120 | syrup 1121 | tacky 1122 | taco 1123 | tag 1124 | take 1125 | tall 1126 | talon 1127 | tamer 1128 | tank 1129 | taper 1130 | taps 1131 | tarot 1132 | tart 1133 | task 1134 | taste 1135 | tasty 1136 | taunt 1137 | thank 1138 | thaw 1139 | theft 1140 | theme 1141 | thigh 1142 | thing 1143 | think 1144 | thong 1145 | thorn 1146 | those 1147 | throb 1148 | thud 1149 | thumb 1150 | thump 1151 | thus 1152 | tiara 1153 | tidal 1154 | tidy 1155 | tiger 1156 | tile 1157 | tilt 1158 | tint 1159 | tiny 1160 | trace 1161 | track 1162 | trade 1163 | train 1164 | trait 1165 | trap 1166 | trash 1167 | tray 1168 | treat 1169 | tree 1170 | trek 1171 | trend 1172 | trial 1173 | tribe 1174 | trick 1175 | trio 1176 | trout 1177 | truce 1178 | truck 1179 | trump 1180 | trunk 1181 | try 1182 | tug 1183 | tulip 1184 | tummy 1185 | turf 1186 | tusk 1187 | tutor 1188 | tutu 1189 | tux 1190 | tweak 1191 | tweet 1192 | twice 1193 | twine 1194 | twins 1195 | twirl 1196 | twist 1197 | uncle 1198 | uncut 1199 | undo 1200 | unify 1201 | union 1202 | unit 1203 | untie 1204 | upon 1205 | upper 1206 | urban 1207 | used 1208 | user 1209 | usher 1210 | utter 1211 | value 1212 | vapor 1213 | vegan 1214 | venue 1215 | verse 1216 | vest 1217 | veto 1218 | vice 1219 | video 1220 | view 1221 | viral 1222 | virus 1223 | visa 1224 | visor 1225 | vixen 1226 | vocal 1227 | voice 1228 | void 1229 | volt 1230 | voter 1231 | vowel 1232 | wad 1233 | wafer 1234 | wager 1235 | wages 1236 | wagon 1237 | wake 1238 | walk 1239 | wand 1240 | wasp 1241 | watch 1242 | water 1243 | wavy 1244 | wheat 1245 | whiff 1246 | whole 1247 | whoop 1248 | wick 1249 | widen 1250 | widow 1251 | width 1252 | wife 1253 | wifi 1254 | wilt 1255 | wimp 1256 | wind 1257 | wing 1258 | wink 1259 | wipe 1260 | wired 1261 | wiry 1262 | wise 1263 | wish 1264 | wispy 1265 | wok 1266 | wolf 1267 | womb 1268 | wool 1269 | woozy 1270 | word 1271 | work 1272 | worry 1273 | wound 1274 | woven 1275 | wrath 1276 | wreck 1277 | wrist 1278 | xerox 1279 | yahoo 1280 | yam 1281 | yard 1282 | year 1283 | yeast 1284 | yelp 1285 | yield 1286 | yodel 1287 | yoga 1288 | yoyo 1289 | yummy 1290 | zebra 1291 | zero 1292 | zesty 1293 | zippy 1294 | zone 1295 | zoom 1296 | -------------------------------------------------------------------------------- /pont.conf: -------------------------------------------------------------------------------- 1 | map $http_upgrade $connection_upgrade { 2 | default upgrade; 3 | '' close; 4 | } 5 | 6 | # Redirect all port 80 communications to SSL 7 | server { 8 | listen 80 default_server; 9 | listen [::]:80 default_server; 10 | server_name _; 11 | return 301 https://$host$request_uri; 12 | } 13 | 14 | server { 15 | root /home/mkeeter/pont/pont-client/deploy; 16 | location / { 17 | try_files $uri $uri/ =404; 18 | } 19 | types { 20 | text/html html; 21 | text/css css; 22 | application/javascript js; 23 | application/wasm wasm; 24 | } 25 | index index.html; 26 | 27 | server_name pont.mattkeeter.com; 28 | listen [::]:443 ssl ipv6only=on; 29 | listen 443 ssl; 30 | ssl_certificate /etc/letsencrypt/live/pont.mattkeeter.com/fullchain.pem; 31 | ssl_certificate_key /etc/letsencrypt/live/pont.mattkeeter.com/privkey.pem; 32 | include /etc/letsencrypt/options-ssl-nginx.conf; 33 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 34 | } 35 | 36 | server { 37 | listen 8081 ssl; 38 | server_name pont.mattkeeter.com; 39 | 40 | ssl_certificate /etc/letsencrypt/live/pont.mattkeeter.com/fullchain.pem; 41 | ssl_certificate_key /etc/letsencrypt/live/pont.mattkeeter.com/privkey.pem; 42 | include /etc/letsencrypt/options-ssl-nginx.conf; 43 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 44 | 45 | location / { 46 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 47 | proxy_set_header Host $host; 48 | proxy_pass http://127.0.0.1:8080; 49 | proxy_http_version 1.1; 50 | proxy_set_header Upgrade $http_upgrade; 51 | proxy_set_header Connection $connection_upgrade; 52 | 53 | # Lazy choice here because we don't want to add pinging to the server 54 | proxy_read_timeout 1d; 55 | proxy_send_timeout 1d; 56 | } 57 | 58 | } 59 | 60 | --------------------------------------------------------------------------------