├── .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 | 
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 | 
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 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Room:
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Player
74 | Score
75 |
76 |
77 |
78 |
79 |
80 |
87 |
88 |
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 |
--------------------------------------------------------------------------------