rework game as a trait
authorGeoffrey Allott <geoffrey@allott.email>
Tue, 23 Feb 2021 20:44:44 +0000 (20:44 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Tue, 23 Feb 2021 20:44:44 +0000 (20:44 +0000)
75 files changed:
Cargo.lock
Cargo.toml
site/img/card-2c.svg [new file with mode: 0644]
site/img/card-2d.svg [new file with mode: 0644]
site/img/card-2h.svg [new file with mode: 0644]
site/img/card-2s.svg [new file with mode: 0644]
site/img/card-3c.svg [new file with mode: 0644]
site/img/card-3d.svg [new file with mode: 0644]
site/img/card-3h.svg [new file with mode: 0644]
site/img/card-3s.svg [new file with mode: 0644]
site/img/card-4c.svg [new file with mode: 0644]
site/img/card-4d.svg [new file with mode: 0644]
site/img/card-4h.svg [new file with mode: 0644]
site/img/card-4s.svg [new file with mode: 0644]
site/img/card-5c.svg [new file with mode: 0644]
site/img/card-5d.svg [new file with mode: 0644]
site/img/card-5h.svg [new file with mode: 0644]
site/img/card-5s.svg [new file with mode: 0644]
site/img/card-6c.svg [new file with mode: 0644]
site/img/card-6d.svg [new file with mode: 0644]
site/img/card-6h.svg [new file with mode: 0644]
site/img/card-6s.svg [new file with mode: 0644]
site/img/card-7c.svg [new file with mode: 0644]
site/img/card-7d.svg [new file with mode: 0644]
site/img/card-7h.svg [new file with mode: 0644]
site/img/card-7s.svg [new file with mode: 0644]
site/img/card-8c.svg [new file with mode: 0644]
site/img/card-8d.svg [new file with mode: 0644]
site/img/card-8h.svg [new file with mode: 0644]
site/img/card-8s.svg [new file with mode: 0644]
site/img/card-9c.svg [new file with mode: 0644]
site/img/card-9d.svg [new file with mode: 0644]
site/img/card-9h.svg [new file with mode: 0644]
site/img/card-9s.svg [new file with mode: 0644]
site/img/card-Ac.svg [new file with mode: 0644]
site/img/card-Ad.svg [new file with mode: 0644]
site/img/card-Ah.svg [new file with mode: 0644]
site/img/card-As.svg [new file with mode: 0644]
site/img/card-Jc.svg [new file with mode: 0644]
site/img/card-Jd.svg [new file with mode: 0644]
site/img/card-Jh.svg [new file with mode: 0644]
site/img/card-Js.svg [new file with mode: 0644]
site/img/card-Kc.svg [new file with mode: 0644]
site/img/card-Kd.svg [new file with mode: 0644]
site/img/card-Kh.svg [new file with mode: 0644]
site/img/card-Ks.svg [new file with mode: 0644]
site/img/card-Qc.svg [new file with mode: 0644]
site/img/card-Qd.svg [new file with mode: 0644]
site/img/card-Qh.svg [new file with mode: 0644]
site/img/card-Qs.svg [new file with mode: 0644]
site/img/card-Tc.svg [new file with mode: 0644]
site/img/card-Td.svg [new file with mode: 0644]
site/img/card-Th.svg [new file with mode: 0644]
site/img/card-Ts.svg [new file with mode: 0644]
site/img/card-back-blue.svg [new file with mode: 0644]
site/modules/card.js [new file with mode: 0644]
site/modules/socket.js
site/style.css
site/style/whist.css [new file with mode: 0644]
site/test.html [new file with mode: 0644]
src/api.rs
src/card.rs
src/client.rs
src/dealer.rs [new file with mode: 0644]
src/game.rs
src/games/chatroom.rs [new file with mode: 0644]
src/games/mod.rs [new file with mode: 0644]
src/games/poker.rs [new file with mode: 0644]
src/games/whist.rs [new file with mode: 0644]
src/gamestate.rs
src/hands.rs [new file with mode: 0644]
src/main.rs
src/seats.rs
src/server.rs
src/username.rs [new file with mode: 0644]

index a6cc0e8352e19fc38d902efbcd636dec5bc31fd5..e466e0c2fcb0bad93f2720a2a17442c4978be2b8 100644 (file)
@@ -565,6 +565,12 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e"
 
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
 [[package]]
 name = "env_logger"
 version = "0.8.2"
@@ -894,6 +900,15 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.7"
@@ -1047,8 +1062,10 @@ dependencies = [
  "async-std",
  "env_logger",
  "futures",
+ "itertools",
  "log",
  "rand 0.8.3",
+ "rand_chacha 0.3.0",
  "redis",
  "serde",
  "serde_derive",
index 521a4f800f62b0519e007b0c640f36b1e5fc54db..bf8e9d104f36a52948b8078b437814fd923a9f53 100644 (file)
@@ -9,7 +9,9 @@ async-std = { version = "1", features = ["attributes"] }
 env_logger = "0.8"
 log = "0.4"
 futures = "0.3"
+itertools = "0.10"
 rand = "0.8"
+rand_chacha = "0.3"
 redis = { version = "0.19", features = ["async-std-comp"] }
 serde = "1"
 serde_derive = "1"
diff --git a/site/img/card-2c.svg b/site/img/card-2c.svg
new file mode 100644 (file)
index 0000000..2afaf55
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">2</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-2d.svg b/site/img/card-2d.svg
new file mode 100644 (file)
index 0000000..d781c5f
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">2</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-2h.svg b/site/img/card-2h.svg
new file mode 100644 (file)
index 0000000..2b5ae9d
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">2</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-2s.svg b/site/img/card-2s.svg
new file mode 100644 (file)
index 0000000..bf9d374
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">2</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-3c.svg b/site/img/card-3c.svg
new file mode 100644 (file)
index 0000000..9cce91c
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">3</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-3d.svg b/site/img/card-3d.svg
new file mode 100644 (file)
index 0000000..87692b0
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">3</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-3h.svg b/site/img/card-3h.svg
new file mode 100644 (file)
index 0000000..78a125f
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">3</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-3s.svg b/site/img/card-3s.svg
new file mode 100644 (file)
index 0000000..d80d75e
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">3</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-4c.svg b/site/img/card-4c.svg
new file mode 100644 (file)
index 0000000..c647371
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">4</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-4d.svg b/site/img/card-4d.svg
new file mode 100644 (file)
index 0000000..f3d8128
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">4</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-4h.svg b/site/img/card-4h.svg
new file mode 100644 (file)
index 0000000..cfaa1d0
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">4</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-4s.svg b/site/img/card-4s.svg
new file mode 100644 (file)
index 0000000..4c9a7f4
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">4</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-5c.svg b/site/img/card-5c.svg
new file mode 100644 (file)
index 0000000..d0822b1
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">5</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-5d.svg b/site/img/card-5d.svg
new file mode 100644 (file)
index 0000000..c7d1d3f
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">5</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-5h.svg b/site/img/card-5h.svg
new file mode 100644 (file)
index 0000000..38c8da3
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">5</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-5s.svg b/site/img/card-5s.svg
new file mode 100644 (file)
index 0000000..1bae79e
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">5</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-6c.svg b/site/img/card-6c.svg
new file mode 100644 (file)
index 0000000..458d98a
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">6</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-6d.svg b/site/img/card-6d.svg
new file mode 100644 (file)
index 0000000..d61f2fc
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">6</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-6h.svg b/site/img/card-6h.svg
new file mode 100644 (file)
index 0000000..eb27710
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">6</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-6s.svg b/site/img/card-6s.svg
new file mode 100644 (file)
index 0000000..b7217ef
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">6</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-7c.svg b/site/img/card-7c.svg
new file mode 100644 (file)
index 0000000..a384abb
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">7</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-7d.svg b/site/img/card-7d.svg
new file mode 100644 (file)
index 0000000..fccc289
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">7</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-7h.svg b/site/img/card-7h.svg
new file mode 100644 (file)
index 0000000..9218ab4
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">7</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-7s.svg b/site/img/card-7s.svg
new file mode 100644 (file)
index 0000000..5771662
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">7</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-8c.svg b/site/img/card-8c.svg
new file mode 100644 (file)
index 0000000..0fc2832
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">8</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-8d.svg b/site/img/card-8d.svg
new file mode 100644 (file)
index 0000000..b91ddcd
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">8</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-8h.svg b/site/img/card-8h.svg
new file mode 100644 (file)
index 0000000..44a54b3
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">8</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-8s.svg b/site/img/card-8s.svg
new file mode 100644 (file)
index 0000000..2a65b28
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">8</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-9c.svg b/site/img/card-9c.svg
new file mode 100644 (file)
index 0000000..a7f9706
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">9</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-9d.svg b/site/img/card-9d.svg
new file mode 100644 (file)
index 0000000..8c86ba2
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">9</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-9h.svg b/site/img/card-9h.svg
new file mode 100644 (file)
index 0000000..bd760ad
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">9</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-9s.svg b/site/img/card-9s.svg
new file mode 100644 (file)
index 0000000..1ad161e
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">9</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Ac.svg b/site/img/card-Ac.svg
new file mode 100644 (file)
index 0000000..8f8178d
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">A</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Ad.svg b/site/img/card-Ad.svg
new file mode 100644 (file)
index 0000000..5547b38
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">A</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Ah.svg b/site/img/card-Ah.svg
new file mode 100644 (file)
index 0000000..4251cac
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">A</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-As.svg b/site/img/card-As.svg
new file mode 100644 (file)
index 0000000..e5ab9af
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">A</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Jc.svg b/site/img/card-Jc.svg
new file mode 100644 (file)
index 0000000..eb1e194
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="175" y="50" style="font: bold 45px serif;">J</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Jd.svg b/site/img/card-Jd.svg
new file mode 100644 (file)
index 0000000..d6e5271
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="175" y="50" style="font: bold 45px serif;" fill="red">J</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Jh.svg b/site/img/card-Jh.svg
new file mode 100644 (file)
index 0000000..609ab30
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="175" y="50" style="font: bold 45px serif;" fill="red">J</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Js.svg b/site/img/card-Js.svg
new file mode 100644 (file)
index 0000000..d61fda4
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="175" y="50" style="font: bold 45px serif;">J</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Kc.svg b/site/img/card-Kc.svg
new file mode 100644 (file)
index 0000000..13f455c
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="160" y="60" style="font: bold 50px serif;">K</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Kd.svg b/site/img/card-Kd.svg
new file mode 100644 (file)
index 0000000..1edcfc2
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="160" y="60" style="font: bold 50px serif;" fill="red">K</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Kh.svg b/site/img/card-Kh.svg
new file mode 100644 (file)
index 0000000..9c70f65
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="160" y="60" style="font: bold 50px serif;" fill="red">K</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Ks.svg b/site/img/card-Ks.svg
new file mode 100644 (file)
index 0000000..03708c6
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="160" y="60" style="font: bold 50px serif;">K</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Qc.svg b/site/img/card-Qc.svg
new file mode 100644 (file)
index 0000000..b08cabd
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">Q</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Qd.svg b/site/img/card-Qd.svg
new file mode 100644 (file)
index 0000000..f095977
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">Q</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Qh.svg b/site/img/card-Qh.svg
new file mode 100644 (file)
index 0000000..a57a540
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;" fill="red">Q</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Qs.svg b/site/img/card-Qs.svg
new file mode 100644 (file)
index 0000000..edc4c07
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="165" y="60" style="font: bold 50px serif;">Q</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Tc.svg b/site/img/card-Tc.svg
new file mode 100644 (file)
index 0000000..0236727
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="150" y="60" style="font: bold 50px serif;" textLength="55px">10</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♣</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Td.svg b/site/img/card-Td.svg
new file mode 100644 (file)
index 0000000..7c492b0
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="150" y="60" style="font: bold 50px serif;" textLength="55px" fill="red">10</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♢</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Th.svg b/site/img/card-Th.svg
new file mode 100644 (file)
index 0000000..3e40f9f
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="150" y="60" style="font: bold 50px serif;" textLength="55px" fill="red">10</text>
+        <text x="150" y="120" style="font: bold 75px serif;" fill="red">♡</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-Ts.svg b/site/img/card-Ts.svg
new file mode 100644 (file)
index 0000000..0bad8c7
--- /dev/null
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <g id="text">
+        <text x="150" y="60" style="font: bold 50px serif;" textLength="55px">10</text>
+        <text x="150" y="120" style="font: bold 75px serif;">♠</text>
+    </g>
+    <use href="#text" transform="rotate(180 112.5 175)" />
+</svg>
diff --git a/site/img/card-back-blue.svg b/site/img/card-back-blue.svg
new file mode 100644 (file)
index 0000000..bef58cd
--- /dev/null
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="225" height="350">
+    <defs>
+        <pattern id="blue-cross" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
+            <rect x="0" y="0" width="40" height="40" fill="#fffff8" />
+            <line x1="0" y1="0" x2="40" y2="40" stroke="#4040a0" stroke-width="5" />
+            <line x1="0" y1="40" x2="40" y2="0" stroke="#4040a0" stroke-width="5" />
+        </pattern>
+    </defs>
+    <rect x="5" y="5" width="215" height="340" fill="#fffff8" stroke="black" stroke-width="5" rx="25" />
+    <rect x="20" y="25" width="185" height="300" fill="url(#blue-cross)" stroke="#4040a0" stroke-width="5" rx="25" />
+</svg>
diff --git a/site/modules/card.js b/site/modules/card.js
new file mode 100644 (file)
index 0000000..2422de5
--- /dev/null
@@ -0,0 +1,31 @@
+export function short_rank(rank) {
+    switch (rank) {
+        case "Ace": return "A";
+        case "Two": return "2";
+        case "Three": return "3";
+        case "Four": return "4";
+        case "Five": return "5";
+        case "Six": return "6";
+        case "Seven": return "7";
+        case "Eight": return "8";
+        case "Nine": return "9";
+        case "Ten": return "T";
+        case "Jack": return "J";
+        case "Queen": return "Q";
+        case "King": return "K";
+    }
+}
+
+export function short_suit(suit) {
+    switch (suit) {
+        case "Clubs": return "c";
+        case "Diamonds": return "d";
+        case "Hearts": return "h";
+        case "Spades": return "s";
+    }
+}
+
+export function card_href(card) {
+    if (card === null) return "img/card-back-blue.svg";
+    return "img/card-" + short_rank(card.rank) + short_suit(card.suit) + ".svg";
+}
index 526a4eefa749348b71f03751696abd189885af4d..2a5051020933c1d67ac32a2e3b1122bc52a16f5c 100644 (file)
@@ -1,3 +1,7 @@
+const svgns = "http://www.w3.org/2000/svg";
+
+import { card_href } from "./card.js";
+
 export class Socket {
     constructor(container, login_all_sockets) {
         this.container = container;
@@ -75,8 +79,12 @@ export class Socket {
                 delete this.action;
                 break;
             case "TakeActionFailure":
-                console.error("Taking action failed: " + message.reason, this.action);
-                delete this.action;
+                if (message.reason === "SeatNotAvailable") {
+                    this.take_action({action: "Join", seat: this.action.seat + 1, chips: this.action.chips});
+                } else {
+                    console.error("Taking action failed: " + message.reason, this.action);
+                    delete this.action;
+                }
                 break;
             case "NewAction":
                 this.add_action(message.action);
@@ -227,6 +235,39 @@ export class Socket {
         this.redraw_chatroom();
     }
 
+    create_knock_out_whist() {
+        this.container.textContent = "";
+
+        this.svg = document.createElementNS(svgns, "svg");
+        this.svg.classList.add("knock-out-whist");
+        this.svg.setAttribute("viewBox", "0 0 500 500");
+
+        const background = document.createElementNS(svgns, "rect");
+        background.setAttribute("width", "500");
+        background.setAttribute("height", "500");
+        background.setAttribute("fill", "#404040");
+        this.svg.append(background);
+
+        const table = document.createElementNS(svgns, "ellipse");
+        table.setAttribute("cx", "250");
+        table.setAttribute("cy", "253");
+        table.setAttribute("rx", "250");
+        table.setAttribute("ry", "110");
+        table.setAttribute("fill", "#604010");
+        this.svg.append(table);
+
+        const felt = document.createElementNS(svgns, "ellipse");
+        felt.setAttribute("cx", "250");
+        felt.setAttribute("cy", "250");
+        felt.setAttribute("rx", "240");
+        felt.setAttribute("ry", "100");
+        felt.setAttribute("fill", "green");
+        this.svg.append(felt);
+
+        this.container.append(this.svg);
+        this.redraw_knock_out_whist();
+    }
+
     join_element(username) {
         const join_element = document.createElement("div");
         join_element.classList.add("chatroom-join");
@@ -269,20 +310,64 @@ export class Socket {
         return leave_element;
     }
 
+    set_card_positions() {
+        const seats = Math.max(...this.game.seats.values()) + 1;
+        const my_seat = this.game.seats.get(this.auth.username);
+        for (const [username, cards] of this.game.hands) {
+            const seat = this.game.seats.get(username);
+            const angle = ((seat - my_seat) % seats) * 2 * Math.PI / seats;
+            const offset = cards.length * 10;
+            let x = 227.5 + offset + 240 * Math.sin(angle);
+            const y = 210 + 120 * Math.cos(angle);
+            for (const {card, image} of cards) {
+                image.classList.toggle("my-card", username === this.auth.username);
+                image.setAttribute("x", x);
+                image.setAttribute("y", y);
+                x -= 20;
+            }
+        }
+        for (const {card, image} of this.game.trick) {
+            const x = 227.5;
+            const y = 210;
+            image.classList.remove("my-card");
+            image.setAttribute("x", x);
+            image.setAttribute("y", y);
+        }
+        for (const {card, image} of this.game.community) {
+            const x = 120.0;
+            const y = 210;
+            image.setAttribute("x", x);
+            image.setAttribute("y", y);
+        }
+    }
+
+    card_image(username, card) {
+        const image = document.createElementNS(svgns, "image");
+        image.setAttribute("width", "45");
+        image.setAttribute("height", "70");
+        image.setAttribute("href", card_href(card));
+        if (username === this.auth.username) {
+            image.onclick = () => this.take_action({action: "PlayCard", card: card});
+        }
+        this.svg.append(image);
+        return image;
+    }
+
     add_action(user_action, initialising) {
         switch (this.game.summary.settings.format) {
             case "Chatroom":
                 const is_at_end = this.chatroom_chat.scrollTop + this.chatroom_chat.clientHeight == this.chatroom_chat.scrollHeight;
                 switch (user_action.action.action) {
                     case "Join":
-                        this.game.users.add(user_action.username);
+                        this.game.seats.set(user_action.username, user_action.action.seat);
                         this.chatroom_chat.append(this.join_element(user_action.username));
                         break;
                     case "Message":
                         this.chatroom_chat.append(this.message_element(user_action.username, user_action.action.message));
                         break;
                     case "Leave":
-                        this.game.users.delete(user_action.username);
+                        this.game.seats.delete(user_action.username);
+                        this.game.hands.delete(user_action.username);
                         this.chatroom_chat.append(this.leave_element(user_action.username));
                         if (!initialising && user_action.username === this.auth.username) {
                             this.send({type: "JoinLobby", filter: ""});
@@ -299,6 +384,74 @@ export class Socket {
                     });
                 }
                 break;
+            case "KnockOutWhist":
+                switch (user_action.action.action) {
+                    case "Join":
+                        this.game.seats.set(user_action.username, user_action.action.seat);
+                        this.game.hands.set(user_action.username, []);
+                        break;
+                    case "Leave":
+                    case "KnockedOut":
+                        this.game.seats.delete(user_action.username);
+                        this.game.hands.delete(user_action.username);
+                        break;
+                    case "NextToDeal":
+                        this.dealer = user_action.username;
+                        break;
+                    case "ReceiveCard":
+                        const card = {
+                            card: user_action.action.card,
+                            image: this.card_image(user_action.username, user_action.action.card),
+                        };
+                        this.game.hands.get(user_action.username).push(card);
+                        this.set_card_positions();
+                        break;
+                    case "PlayCard":
+                        let removed = false;
+                        const cards = this.game.hands.get(user_action.username).filter((card) => {
+                            if (card.card === null) {
+                                if (!removed) {
+                                    this.svg.removeChild(card.image);
+                                    this.game.trick.push({card: user_action.action.card, image: this.card_image(user_action.username, user_action.action.card)});
+                                    removed = true;
+                                    return false;
+                                } else {
+                                    return true;
+                                }
+                            } else if (card.card.suit === user_action.action.card.suit && card.card.rank === user_action.action.card.rank) {
+                                this.game.trick.push(card);
+                                return false;
+                            } else {
+                                return true;
+                            }
+                        });
+                        this.game.hands.set(user_action.username, cards);
+                        this.set_card_positions();
+                    case "ChooseTrumps":
+                        this.game.trump_suit = user_action.action.suit;
+                        break;
+                    case "CommunityCard":
+                        const community_card = {
+                            card: user_action.action.card,
+                            image: this.card_image(null, user_action.action.card),
+                        };
+                        this.game.community.push(community_card);
+                        this.game.trump_suit = user_action.action.card.suit;
+                        break;
+                    case "WinTrick":
+                        for (const card of this.game.trick) {
+                            this.svg.removeChild(card.image);
+                        }
+                        this.game.trick = [];
+                        break;
+                    case "EndDeal":
+                        console.log(this);
+                        break;
+                    default:
+                        console.error("Unhandled action for knock-out whist", user_action);
+                        break;
+                }
+                break;
         }
     }
 
@@ -309,13 +462,29 @@ export class Socket {
         }
     }
 
+    redraw_knock_out_whist() {
+        for (const user_action of this.game.state.actions) {
+            this.add_action(user_action, true);
+        }
+    }
+
     create_game_display() {
         this.container.textContent = "";
         switch (this.game.summary.settings.format) {
             case "Chatroom":
-                this.game.users = new Set();
+                this.game.seats = new Map();
                 this.create_chatroom();
-                if (!this.game.users.has(this.auth.username)) {
+                if (!this.game.seats.has(this.auth.username)) {
+                    this.take_action({action: "Join", seat: 0, chips: 0});
+                }
+                break;
+            case "KnockOutWhist":
+                this.game.seats = new Map();
+                this.game.hands = new Map();
+                this.game.community = [];
+                this.game.trick = [];
+                this.create_knock_out_whist();
+                if (!this.game.seats.has(this.auth.username)) {
                     this.take_action({action: "Join", seat: 0, chips: 0});
                 }
                 break;
index 1528a1db0e673e43e7796b7599fd97551d0fc8cf..521256000105079b200e964afc69ee33aaa1ad36 100644 (file)
@@ -1,6 +1,7 @@
 @import url("style/login.css");
 @import url("style/game-list.css");
 @import url("style/chatroom.css");
+@import url("style/whist.css");
 
 html, body {
     width: 100%;
diff --git a/site/style/whist.css b/site/style/whist.css
new file mode 100644 (file)
index 0000000..7c8d47f
--- /dev/null
@@ -0,0 +1,13 @@
+svg {
+    width: 100%;
+    height: 100%;
+}
+
+.my-card {
+    transform: none;
+    transition: transform 0.5s;
+}
+
+.my-card:hover {
+    transform: translateY(-20px);
+}
diff --git a/site/test.html b/site/test.html
new file mode 100644 (file)
index 0000000..aea30b4
--- /dev/null
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <link rel="stylesheet" href="./style.css" />
+    </head>
+    <body>
+        <div id="game-tile-background">
+            <svg class="knock-out-whist" viewbox="0 0 500 500">
+                <rect x="0" y="0" width="500" height="500" fill="#404040" />
+                <ellipse cx="250" cy="253" rx="250" ry="110" fill="#604010" />
+                <ellipse cx="250" cy="250" rx="240" ry="100" fill="green" />
+                <image x="240" y="290" width="45" height="70" href="img/card-Kd.svg" />
+                <image x="220" y="290" width="45" height="70" href="img/card-Ks.svg" />
+                <image x="220" y="80" width="45" height="70" href="img/card-back-blue.svg" />
+                <image x="240" y="80" width="45" height="70" href="img/card-back-blue.svg" />
+            </svg>
+        </div>
+    </body>
+</html>
index c4f464c994fd0ad45085342f2798a66c3a5e9a8c..60804214e9444264dc629ea86ebf28b0d9184e0c 100644 (file)
@@ -1,5 +1,7 @@
 use crate::auth::Auth;
-use crate::game::{Action, Game, GameSettings, GameSummary, UserAction};
+use crate::game::{Action, GameSettings, GameSummary};
+use crate::games::UserAction;
+use crate::username::Username;
 
 #[derive(Debug, Clone, Deserialize)]
 pub enum Scope {
@@ -12,8 +14,8 @@ pub enum Scope {
 #[derive(Debug, Clone, Deserialize)]
 #[serde(tag = "type")]
 pub enum ClientMessage {
-    CreateUser { username: String, auth: Auth, nickname: String },
-    Login { username: String },
+    CreateUser { username: Username, auth: Auth, nickname: String },
+    Login { username: Username },
     LoginAuthResponse { signature: String },
     ChangeAuth { auth: Auth },
     ChangeNickname { nickname: String },
@@ -46,16 +48,16 @@ pub enum ServerMessage {
     JoinLobbySuccess { games: Vec<GameSummary> },
     JoinLobbyFailure { reason: String },
     NewGame { game: GameSummary },
-    JoinGameSuccess { game: Game },
+    JoinGameSuccess { summary: GameSummary, actions: Vec<UserAction> },
     JoinGameFailure { reason: String },
     NewAction { action: UserAction },
     TakeActionSuccess,
     TakeActionFailure { reason: String },
-    NewMessage { username: String, message: String },
+    NewMessage { username: Username, message: String },
     LeaveGameSuccess,
     LeaveGameFailure { reason: String },
     LeaveLobbySuccess,
     LeaveLobbyFailure { reason: String },
-    HandHistory { games: Vec<Game> },
+    HandHistory { games: Vec<(GameSummary, Vec<UserAction>)> }, // TODO
     ProtocolError { reason: String },
 }
index f7d061e9d5e9e02d0d0ee577186f62198d3ec615..aa09f78bebbe394fb808fe8bcf6ae06d7cdd9e55 100644 (file)
@@ -3,7 +3,7 @@ use std::fmt::{self, Display, Formatter};
 use self::Rank::*;
 use self::Suit::*;
 
-#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub enum Rank {
     Two = 2,
     Three = 3,
@@ -40,7 +40,7 @@ impl Display for Rank {
     }
 }
 
-#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub enum Suit {
     Clubs,
     Diamonds,
@@ -59,7 +59,7 @@ impl Display for Suit {
     }
 }
 
-#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)]
+#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
 pub struct Card {
     pub rank: Rank,
     pub suit: Suit,
index 9fb6a24cf82a8fadceeccb985b18b3f79c962b8a..45a2be02e9762803d5c703ee35248d45f4c3f01f 100644 (file)
@@ -3,8 +3,10 @@ use std::collections::HashSet;
 use futures::stream::{Stream, StreamExt, empty, iter, once};
 
 use crate::api::{ClientMessage, ServerMessage};
-use crate::game::{Game, GameList, UserAction};
+use crate::game::GameList;
+use crate::games::{Game, UserAction};
 use crate::server::{ActionStatus, ServerState};
+use crate::username::Username;
 
 pub struct ConnectionState {
     server: ServerState,
@@ -14,9 +16,9 @@ pub struct ConnectionState {
 #[derive(Debug, Clone)]
 pub enum ClientState {
     Connected,
-    LoginAuthIssued { username: String, challenge: String },
+    LoginAuthIssued { username: Username, challenge: String },
     LoggedIn {
-        username: String,
+        username: Username,
         state: LoggedInState,
     },
 }
@@ -25,14 +27,14 @@ pub enum ClientState {
 pub enum ClientInterest {
     GameList,
     Game { id: u32 },
-    User { username: String },
+    User { username: Username },
 }
 
 #[derive(Debug, Clone)]
 pub enum LoggedInState {
     Idle,
     InLobby { game_list: GameList },
-    InGame { game: Game },
+    InGame { game: Box<dyn Game> },
 }
 
 impl ConnectionState {
@@ -56,10 +58,17 @@ impl ConnectionState {
                 _ => empty().boxed(),
             }
             ClientInterest::Game{id} => match &mut self.client {
-                ClientState::LoggedIn{ref username, state: LoggedInState::InGame{ref mut game}} if game.id() == id => {
+                &mut ClientState::LoggedIn{username, state: LoggedInState::InGame{ref mut game}} if game.id() == id => {
+                    let id = game.id();
                     let from = game.actions_len();
-                    match self.server.update_game_state(game).await {
-                        Ok(()) => iter(game.update_view_for(username, from)).map(|action| ServerMessage::NewAction{action}).boxed(),
+                    match self.server.game_state(id, from).await {
+                        Ok(actions) => {
+                            let actions_view: Vec<_> = actions.iter().map(|action| action.view_for(username)).collect();
+                            for action in actions {
+                                game.take_action(action);
+                            }
+                            iter(actions_view).map(|action| ServerMessage::NewAction{action}).boxed()
+                        }
                         Err(err) => once(async move { ServerMessage::ProtocolError{reason: err.to_string()} }).boxed(),
                     }
                 }
@@ -71,8 +80,7 @@ impl ConnectionState {
 
     pub fn interests(&self) -> HashSet<ClientInterest> {
         let mut ret = HashSet::new();
-        if let ClientState::LoggedIn{ref username, ref state} = &self.client {
-            let username = username.to_string();
+        if let &ClientState::LoggedIn{username, ref state} = &self.client {
             ret.insert(ClientInterest::User{username});
             match state {
                 LoggedInState::Idle => {},
@@ -82,7 +90,6 @@ impl ConnectionState {
                 LoggedInState::InGame{ref game} => {
                     ret.insert(ClientInterest::Game{id: game.id()});
                     for username in game.players() {
-                        let username = username.to_string();
                         ret.insert(ClientInterest::User{username});
                     }
                 }
@@ -105,18 +112,18 @@ impl ConnectionState {
     async fn message_response(&mut self, message: ClientMessage) -> ServerMessage {
         match (&mut self.client, message) {
             (_, ClientMessage::CreateUser{username, auth, nickname}) => {
-                match self.server.create_user(&username, auth, &nickname).await {
+                match self.server.create_user(username, auth, &nickname).await {
                     Ok(()) => ServerMessage::CreateUserSuccess,
                     Err(_) => ServerMessage::CreateUserFailure{reason: "User already exists".to_string()},
                 }
             }
-            (ClientState::Connected, ClientMessage::Login{username}) => {
+            (&mut ClientState::Connected, ClientMessage::Login{username}) => {
                 let challenge = format!("{:032x}{:032x}", rand::random::<u128>(), rand::random::<u128>());
                 self.client = ClientState::LoginAuthIssued{username, challenge: challenge.clone()};
                 ServerMessage::LoginAuthChallenge{challenge}
             }
-            (ClientState::LoginAuthIssued{username, challenge}, ClientMessage::LoginAuthResponse{signature}) => {
-                if self.server.verify(&username, &challenge, &signature).await {
+            (&mut ClientState::LoginAuthIssued{username, ref challenge}, ClientMessage::LoginAuthResponse{signature}) => {
+                if self.server.verify(username, &challenge, &signature).await {
                     self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
                     ServerMessage::LoginSuccess
                 } else {
@@ -124,7 +131,7 @@ impl ConnectionState {
                     ServerMessage::LoginFailure{reason: "Invalid username or password".to_string()}
                 }
             }
-            (ClientState::LoggedIn{username, ..}, ClientMessage::JoinLobby{filter}) => {
+            (&mut ClientState::LoggedIn{username, ..}, ClientMessage::JoinLobby{filter}) => {
                 let mut game_list = GameList::new(filter);
                 match self.server.update_game_list(&mut game_list).await {
                     Ok(()) => {
@@ -135,67 +142,79 @@ impl ConnectionState {
                     Err(err) => ServerMessage::JoinLobbyFailure{reason: err.to_string()},
                 }
             }
-            (ClientState::LoggedIn{..}, ClientMessage::CreateGame{settings}) => {
+            (&mut ClientState::LoggedIn{..}, ClientMessage::CreateGame{settings}) => {
                 match self.server.create_game(settings).await {
                     Ok(id) => ServerMessage::CreateGameSuccess{id},
                     Err(err) => ServerMessage::CreateGameFailure{reason: err.to_string()},
                 }
             }
-            (ClientState::LoggedIn{username, ..}, ClientMessage::JoinGame{id}) => {
-                match self.server.get_game(id).await {
-                    Ok(game) => {
-                        let game_view = game.view_for(&username);
+            (&mut ClientState::LoggedIn{username, ..}, ClientMessage::JoinGame{id}) => {
+                match (self.server.game_summary(id).await, self.server.game_state(id, 0).await) {
+                    (Ok(summary), Ok(actions)) => {
+                        let actions_view = actions.iter().map(|action| action.view_for(username)).collect();
+                        let mut game = Game::new(summary.clone());
+                        for action in actions {
+                            if let Err(err) = game.take_action(action) {
+                                error!("Action from database failed to apply: {}", err);
+                            }
+                        }
                         self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::InGame{game}};
-                        ServerMessage::JoinGameSuccess{game: game_view}
+                        ServerMessage::JoinGameSuccess{summary, actions: actions_view}
                     }
-                    Err(err) => ServerMessage::JoinGameFailure{reason: err.to_string()},
+                    (Err(err), _) => ServerMessage::JoinGameFailure{reason: err.to_string()},
+                    (_, Err(err)) => ServerMessage::JoinGameFailure{reason: err.to_string()},
                 }
             }
-            (ClientState::LoggedIn{ref username, state: LoggedInState::InGame{ref mut game}}, ClientMessage::TakeAction{action}) => {
-                if action.is_initiated_server_side_only() {
-                    return ServerMessage::TakeActionFailure{reason: "Not authorised".to_string()};
-                }
-                let action = UserAction{username: username.clone(), action};
+            (&mut ClientState::LoggedIn{username, state: LoggedInState::InGame{ref mut game}}, ClientMessage::TakeAction{action}) => {
+                let action = UserAction{username, action};
+                let id = game.id();
                 loop {
                     let len = game.actions_len();
-                    if let Err(err) = game.verify(&action) {
-                        return ServerMessage::TakeActionFailure{reason: err.to_string()};
-                    }
-                    match self.server.take_action(game.id(), len, &action).await {
-                        Ok(ActionStatus::Committed) => match game.take_action(action) {
-                            Ok(()) => return ServerMessage::TakeActionSuccess,
-                            Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
-                        }
-                        Ok(ActionStatus::Interrupted) => {
-                            debug!("Action {:?} was interrupted - updating game state", action);
-                            if let Err(err) = self.server.update_game_state(game).await {
-                                return ServerMessage::TakeActionFailure{reason: err.to_string()};
+                    match game.validate_action(action.clone()) {
+                        Ok(action) => match self.server.take_action(id, len, &action).await {
+                            Ok(ActionStatus::Committed) => match game.take_action(action) {
+                                Ok(()) => return ServerMessage::TakeActionSuccess,
+                                Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
                             }
+                            Ok(ActionStatus::Interrupted) => {
+                                debug!("Action {:?} was interrupted - updating game state", action);
+                                match self.server.game_state(id, len).await {
+                                    Ok(actions) => for action in actions {
+                                        if let Err(err) = game.take_action(action) {
+                                            return ServerMessage::TakeActionFailure{reason: err.to_string()};
+                                        }
+                                    }
+                                    Err(err) => {
+                                        return ServerMessage::TakeActionFailure{reason: err.to_string()};
+                                    }
+                                }
+                            }
+                            Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
                         }
                         Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
                     }
                 }
             }
-            (ClientState::LoggedIn{username, state: LoggedInState::InLobby{..}}, ClientMessage::LeaveLobby) => {
-                self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
+            (&mut ClientState::LoggedIn{username, state: LoggedInState::InLobby{..}}, ClientMessage::LeaveLobby) => {
+                self.client = ClientState::LoggedIn{username, state: LoggedInState::Idle};
                 ServerMessage::LeaveGameSuccess
             }
-            (ClientState::LoggedIn{username, state: LoggedInState::InGame{..}}, ClientMessage::LeaveGame) => {
-                self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
+            (&mut ClientState::LoggedIn{username, state: LoggedInState::InGame{..}}, ClientMessage::LeaveGame) => {
+                self.client = ClientState::LoggedIn{username, state: LoggedInState::Idle};
                 ServerMessage::LeaveGameSuccess
             }
-            (ClientState::LoggedIn{..}, ClientMessage::Logout) => {
+            (&mut ClientState::LoggedIn{..}, ClientMessage::Logout) => {
                 self.client = ClientState::Connected;
                 ServerMessage::LogoutSuccess
             }
-            (ClientState::LoggedIn{username, ..}, ClientMessage::ChangeAuth{auth}) => {
-                match self.server.set_user_auth(&username, auth).await {
+            (&mut ClientState::LoggedIn{username, ..}, ClientMessage::ChangeAuth{auth}) => {
+                match self.server.set_user_auth(username, auth).await {
                     Ok(()) => ServerMessage::ChangeAuthSuccess,
                     Err(err) => ServerMessage::ChangeAuthFailure{reason: err.to_string()},
                 }
             }
-            (ClientState::LoggedIn{username, ..}, ClientMessage::ChangeNickname{nickname}) => {
-                match self.server.set_user_nickname(&username, &nickname).await {
+            (&mut ClientState::LoggedIn{username, ..}, ClientMessage::ChangeNickname{nickname}) => {
+                match self.server.set_user_nickname(username, &nickname).await {
                     Ok(()) => ServerMessage::ChangeNicknameSuccess,
                     Err(err) => ServerMessage::ChangeNicknameFailure{reason: err.to_string()},
                 }
diff --git a/src/dealer.rs b/src/dealer.rs
new file mode 100644 (file)
index 0000000..3a2ebc1
--- /dev/null
@@ -0,0 +1,89 @@
+use std::collections::{HashMap, HashSet};
+
+use async_std::stream::StreamExt;
+use futures::channel::mpsc::Receiver;
+use rand::prelude::*;
+use redis::{ErrorKind, RedisError, RedisResult};
+
+use crate::client::ClientInterest;
+use crate::game::Action;
+use crate::games::{Game, ValidatedUserAction};
+use crate::server::{ActionStatus, ServerState};
+
+pub struct Dealer {
+    server: ServerState,
+    dealer: DealerState,
+}
+
+#[derive(Debug, Clone)]
+pub struct DealerState {
+    game: Box<dyn Game>,
+}
+
+impl Dealer {
+    pub async fn new(mut server: ServerState, id: u32) -> RedisResult<Self> {
+        let mut interests = HashSet::new();
+        interests.insert(ClientInterest::Game{id});
+        server.register_interests(interests).await;
+        let summary = server.game_summary(id).await?;
+        let actions = server.game_state(id, 0).await?;
+        let mut game = Game::new(summary);
+        for action in actions {
+            if let Err(err) = game.take_action(action) {
+                error!("Action from database failed to apply: {}", err);
+            }
+        }
+        let mut dealer = Dealer{server, dealer: DealerState{game}};
+        dealer.retrieve_updates().await?;
+        Ok(dealer)
+    }
+
+    pub async fn start(mut self, mut update_stream: Receiver<ClientInterest>) {
+        while let Some(_) = update_stream.next().await {
+            match self.retrieve_updates().await {
+                Ok(()) => continue,
+                Err(err) => {
+                    error!("Could not retrieve updates: {}", err);
+                    break;
+                }
+            }
+        }
+    }
+
+    pub async fn retrieve_updates(&mut self) -> RedisResult<()> {
+        let id = self.dealer.game.id();
+        'retrieve_updates: loop {
+            let from = self.dealer.game.actions_len();
+            let actions = self.server.game_state(id, from).await?;
+            for action in actions {
+                if let Err(err) = self.dealer.game.take_action(action) {
+                    error!("Action from database failed to apply: {}", err);
+                }
+            }
+            'take_action: loop {
+                match self.dealer.game.next_dealer_action() {
+                    Some(action) => match self.take_action(action).await {
+                        Ok(ActionStatus::Committed) => continue 'take_action,
+                        Ok(ActionStatus::Interrupted) => continue 'retrieve_updates,
+                        Err(err) => return Err(err),
+                    },
+                    None => return Ok(()),
+                }
+            }
+        }
+    }
+
+    async fn take_action(&mut self, action: ValidatedUserAction) -> RedisResult<ActionStatus> {
+        let game = &mut self.dealer.game;
+        match self.server.take_action(game.id(), game.actions_len(), &action).await? {
+            ActionStatus::Committed => match game.take_action(action) {
+                Ok(()) => return Ok(ActionStatus::Committed),
+                Err(err) => return Err(RedisError::from((ErrorKind::ClientError, "Invalid action", err.to_string()))),
+            }
+            ActionStatus::Interrupted => {
+                debug!("Action {:?} was interrupted", action);
+                Ok(ActionStatus::Interrupted)
+            }
+        }
+    }
+}
index 3e93b9ea9ebcab5e3a3b7c6a67ab066e83a0073c..52452fedf17157c9d38faad2cddc47c515823443 100644 (file)
@@ -64,6 +64,7 @@ pub enum Action {
     WinGame,
     Message { message: String },
     Leave,
+    KnockedOut,
 }
 
 impl Action {
@@ -73,15 +74,6 @@ impl Action {
             action => action.clone(),
         }
     }
-
-    pub fn is_initiated_server_side_only(&self) -> bool {
-        match self {
-            Action::Join{..} | Action::AddOn{..} | Action::RevealCard{..} | Action::PlayCard{..} |
-            Action::ChooseTrumps{..} | Action::Fold | Action::Bet{..} | Action::Message{..} | Action::Leave => false,
-            Action::NextToDeal | Action::CommunityCard{..} | Action::ReceiveCard{..} | Action::EndDeal |
-            Action::WinTrick | Action::WinHand{..} | Action::WinGame => true,
-        }
-    }
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -104,6 +96,7 @@ pub enum ActionError {
     NotAuthorised,
     AlreadyJoined,
     GameHasStarted,
+    SeatNotAvailable,
     NoSeatAvailable,
     OutOfTurn,
     CardNotPlayable,
@@ -118,6 +111,7 @@ impl Display for ActionError {
             ActionError::NotAuthorised => f.write_str("NotAuthorised"),
             ActionError::AlreadyJoined => f.write_str("AlreadyJoined"),
             ActionError::GameHasStarted => f.write_str("GameHasStarted"),
+            ActionError::SeatNotAvailable => f.write_str("SeatNotAvailable"),
             ActionError::NoSeatAvailable => f.write_str("NoSeatAvailable"),
             ActionError::OutOfTurn => f.write_str("OutOfTurn"),
             ActionError::CardNotPlayable => f.write_str("CardNotPlayable"),
@@ -136,6 +130,10 @@ impl GameSummary {
     pub fn new(id: u32, settings: GameSettings) -> Self {
         Self{id, settings}
     }
+
+    pub fn id(&self) -> u32 {
+        self.id
+    }
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -164,6 +162,34 @@ impl Game {
         self.state.players()
     }
 
+    pub fn players_len(&self) -> u32 {
+        self.state.players_len()
+    }
+
+    pub fn deck(&self) -> HashSet<Card> {
+        self.state.deck()
+    }
+
+    pub fn has_started(&self) -> bool {
+        self.state.has_started()
+    }
+
+    pub fn dealing_to(&self) -> Option<String> {
+        self.state.dealing_to()
+    }
+
+    pub fn dealer(&self) -> Option<String> {
+        self.state.dealer()
+    }
+
+    pub fn all_players_have_played_a_card(&self) -> bool {
+        self.state.all_players_have_played_a_card()
+    }
+
+    pub fn trick_winner(&self) -> Option<String> {
+        self.state.trick_winner()
+    }
+
     pub fn view_for(&self, username: &str) -> Game {
         Game {
             summary: self.summary.clone(),
@@ -179,34 +205,37 @@ impl Game {
         debug!("Verifying action: UserAction {{ username: {:?}, action: {:?} }}", username, action);
         match self.summary.settings {
             GameSettings::Chatroom{max_players, ..} => match action {
-                Action::Join{seat: 0, chips: 0} if self.state.user_has_joined(username) => Err(ActionError::AlreadyJoined),
-                Action::Join{seat: 0, chips: 0} if self.state.num_players() + 1 > max_players => Err(ActionError::NoSeatAvailable),
-                Action::Join{seat: 0, chips: 0} => Ok(()),
-                Action::Message{..} if self.state.user_has_joined(username) => Ok(()),
+                Action::Join{chips: 0, ..} if self.state.player_has_joined(username) => Err(ActionError::AlreadyJoined),
+                Action::Join{chips: 0, ..} if self.state.players_len() + 1 > max_players => Err(ActionError::NoSeatAvailable),
+                Action::Join{seat, chips: 0} if !self.state.seat_is_available(*seat) => Err(ActionError::SeatNotAvailable),
+                Action::Join{chips: 0, ..} => Ok(()),
+                Action::Message{..} if self.state.player_has_joined(username) => Ok(()),
                 Action::Message{..} => Err(ActionError::NotAuthorised),
-                Action::Leave if self.state.user_has_joined(username) => Ok(()),
+                Action::Leave if self.state.player_has_joined(username) => Ok(()),
                 Action::Leave => Err(ActionError::NotAuthorised),
                 _ => Err(ActionError::InvalidActionForGameType),
             },
             GameSettings::KnockOutWhist{max_players, ..} => match action {
-                Action::Join{seat, chips: 0} if self.state.user_has_joined(username) => Err(ActionError::AlreadyJoined),
+                Action::Join{seat, chips: 0} if self.state.player_has_joined(username) => Err(ActionError::AlreadyJoined),
                 Action::Join{seat, chips: 0} if self.state.has_started() => Err(ActionError::GameHasStarted),
-                Action::Join{seat, chips: 0} if !self.state.seat_is_available(*seat) => Err(ActionError::NoSeatAvailable),
+                Action::Join{seat, chips: 0} if *seat >= max_players => Err(ActionError::NoSeatAvailable),
+                Action::Join{seat, chips: 0} if !self.state.seat_is_available(*seat) => Err(ActionError::SeatNotAvailable),
                 Action::Join{seat, chips: 0} => Ok(()),
-                Action::Leave if self.state.user_has_joined(username) && !self.state.has_started() => Ok(()),
-                Action::Leave if self.state.user_has_joined(username) => Err(ActionError::GameHasStarted),
+                Action::Leave if self.state.player_has_joined(username) && !self.state.has_started() => Ok(()),
+                Action::Leave if self.state.player_has_joined(username) => Err(ActionError::GameHasStarted),
                 Action::Leave => Err(ActionError::NotAuthorised),
                 Action::ReceiveCard{card: Some(card)} if self.state.is_dealing_to(username) => Ok(()),
                 Action::ReceiveCard{..} => Err(ActionError::OutOfTurn),
                 Action::NextToDeal => Ok(()),
-                Action::CommunityCard{..} if self.state.is_dealing_community_cards() => Ok(()),
-                Action::CommunityCard{..} => Err(ActionError::OutOfTurn),
-                Action::PlayCard{card} if self.state.player_is_active(username) && self.state.player_has_card(username, *card) => Ok(()),
+                Action::EndDeal => Ok(()),
+                Action::CommunityCard{..} => Ok(()),
+                Action::PlayCard{card} if self.state.player_is_active(username) && !self.state.all_players_have_played_a_card() && self.state.player_has_card(username, *card) => Ok(()),
                 Action::PlayCard{card} if !self.state.player_is_active(username) => Err(ActionError::OutOfTurn),
                 Action::PlayCard{..} => Err(ActionError::CardNotPlayable),
                 Action::WinTrick{..} => Ok(()),
                 Action::WinHand{..} => Ok(()),
                 Action::WinGame => Ok(()),
+                Action::KnockedOut => Ok(()),
                 _ => Err(ActionError::InvalidActionForGameType),
             }
         }
@@ -215,9 +244,9 @@ impl Game {
     pub fn take_action(&mut self, user_action: UserAction) -> Result<(), ActionError> {
         self.verify(&user_action)?;
         debug!("Taking action: {:?}", user_action);
-        debug!("State before: {:?}", self.state);
+        //debug!("State before: {:?}", self.state);
         self.state.take_action(user_action);
-        debug!("State after: {:?}", self.state);
+        //debug!("State after: {:?}", self.state);
         Ok(())
     }
 }
diff --git a/src/games/chatroom.rs b/src/games/chatroom.rs
new file mode 100644 (file)
index 0000000..8bb476c
--- /dev/null
@@ -0,0 +1,92 @@
+use std::collections::HashSet;
+
+use crate::username::{DEALER, Username};
+use crate::game::{Action, ActionError};
+
+use super::{Game, UserAction};
+use super::ValidatedUserAction;
+
+#[derive(Debug, Clone)]
+enum ChatroomAction {
+    Join,
+    Message(String),
+    Leave,
+}
+
+#[derive(Debug, Clone)]
+pub struct ChatroomSettings {
+    title: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct Chatroom {
+    id: u32,
+    settings: ChatroomSettings,
+    messages: Vec<(Username, ChatroomAction)>,
+    users: HashSet<Username>,
+}
+
+impl Chatroom {
+    pub fn new(id: u32, settings: ChatroomSettings) -> Self {
+        Chatroom {
+            id,
+            settings,
+            messages: Vec::new(),
+            users: HashSet::new(),
+        }
+    }
+}
+
+impl Game for Chatroom {
+    fn id(&self) -> u32 {
+        self.id
+    }
+
+    fn players(&self) -> HashSet<Username> {
+        self.users.clone()
+    }
+
+    fn actions_len(&self) -> usize {
+        self.messages.len()
+    }
+
+    fn validate_action(&self, action: UserAction) -> Result<ValidatedUserAction, ActionError> {
+        match action.action {
+            Action::Join{..} if !self.users.contains(&action.username) => Ok(ValidatedUserAction(action)),
+            Action::Join{..} => Err(ActionError::AlreadyJoined),
+            Action::Message{..} if self.users.contains(&action.username) => Ok(ValidatedUserAction(action)),
+            Action::Message{..} => Err(ActionError::NotAuthorised),
+            Action::Leave if self.users.contains(&action.username) => Ok(ValidatedUserAction(action)),
+            Action::Leave => Err(ActionError::NotAuthorised),
+            _ => Err(ActionError::InvalidActionForGameType),
+        }
+    }
+
+    fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError> {
+        let ValidatedUserAction(action) = action;
+        match action.action {
+            Action::Join{..} => {
+                self.messages.push((action.username, ChatroomAction::Join));
+                self.users.insert(action.username);
+                Ok(())
+            }
+            Action::Message{message} => {
+                self.messages.push((action.username, ChatroomAction::Message(message)));
+                Ok(())
+            }
+            Action::Leave => {
+                self.messages.push((action.username, ChatroomAction::Leave));
+                self.users.remove(&action.username);
+                Ok(())
+            }
+            _ => Err(ActionError::InvalidActionForGameType),
+        }
+    }
+
+    fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
+        match self.messages.len() {
+            n if n % 10 == 0 => Some(ValidatedUserAction(UserAction{username: DEALER, action: Action::Message{message: format!("{} messages posted so far", n)}})),
+            _ => None,
+        }
+    }
+}
diff --git a/src/games/mod.rs b/src/games/mod.rs
new file mode 100644 (file)
index 0000000..8527842
--- /dev/null
@@ -0,0 +1,57 @@
+pub mod chatroom;
+
+use std::collections::HashSet;
+use std::fmt::Debug;
+
+use crate::game::{Action, ActionError, GameSummary};
+use crate::username::Username;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct ValidatedUserAction(UserAction);
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UserAction {
+    pub username: Username,
+    pub action: Action,
+}
+
+impl ValidatedUserAction {
+    pub fn view_for(&self, username: Username) -> UserAction {
+        UserAction {
+            username: self.0.username.clone(),
+            action: if username == self.0.username { self.0.action.clone() } else { self.0.action.anonymise() },
+        }
+    }
+}
+
+pub trait Game : Debug + CloneBox + Send + Sync {
+    fn id(&self) -> u32;
+    fn players(&self) -> HashSet<Username>;
+    fn actions_len(&self) -> usize;
+    fn validate_action(&self, action: UserAction) -> Result<ValidatedUserAction, ActionError>;
+    fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError>;
+    fn next_dealer_action(&self) -> Option<ValidatedUserAction>;
+}
+
+pub trait CloneBox {
+    fn clone_box(&self) -> Box<dyn Game>;
+}
+
+impl<T: Clone + Game + 'static> CloneBox for T {
+    fn clone_box(&self) -> Box<dyn Game> {
+        Box::new(self.clone())
+    }
+}
+
+impl Clone for Box<dyn Game> {
+    fn clone(&self) -> Self {
+        self.clone_box()
+    }
+}
+
+impl dyn Game {
+    pub fn new(summary: GameSummary) -> Box<Self> {
+        todo!()
+    }
+}
diff --git a/src/games/poker.rs b/src/games/poker.rs
new file mode 100644 (file)
index 0000000..ba08f22
--- /dev/null
@@ -0,0 +1,79 @@
+pub enum TexasHoldEm {
+    NotYetStarted {
+        seats: Seats,
+        stacks: HashMap<Username, u32>,
+    }
+    Dealing {
+        dealer: Username,
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        stacks: HashMap<Username, u32>,
+    }
+    PostSmallBlind {
+        dealer: Username,
+        action: Username,
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        bets: HashMap<Username, u32>,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    PostBigBlind {
+        dealer: Username,
+        action: Username,
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        bets: HashMap<Username, u32>,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    PreFlopBetting {
+        dealer: Username,
+        action: Username,
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        pot: u32,
+        bets: HashMap<Username, u32>,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    DealFirstFlopCard {
+        dealer: Username,
+        action: Username,
+        flop: [Card; 1],
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        pot: u32,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    DealSecondFlopCard {
+        dealer: Username,
+        action: Username,
+        flop: [Card; 2],
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        pot: u32,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    DealThirdFlopCard {
+        dealer: Username,
+        action: Username,
+        flop: [Card; 3],
+        hands: HashMap<Username, HashSet<Card>>,
+        deck: HashSet<Card>,
+        seats: Seats,
+        pot: u32,
+        players: HashSet<Username>,
+        stacks: HashMap<Username, u32>,
+    }
+    PostFlopBetting {
+    }
+}
diff --git a/src/games/whist.rs b/src/games/whist.rs
new file mode 100644 (file)
index 0000000..a4ad244
--- /dev/null
@@ -0,0 +1,36 @@
+pub enum KnockOutWhist {
+    NotYetStarted {
+        seats: Seats,
+    }
+    Dealing {
+        dealer: Username,
+        call: Username,
+        deck: HashSet<Card>,
+        hands: HashMap<Username, HashSet<Card>>,
+        trump_card: Option<Card>,
+        seats: Seats,
+        cards_to_deal: u32,
+    }
+    ChoosingTrumps {
+        dealer: Username,
+        call: Username,
+        hands: HashMap<Username, HashSet<Card>>,
+        seats: Seats,
+    }
+    Playing {
+        turn: Username,
+        trumps: Suit,
+        trick: HashMap<Username, Card>,
+        hands: HashMap<Username, HashSet<Card>>,
+        seats: Seats,
+        tricks_won: HashMap<Username, u32>,
+    }
+    CutForCall {
+        winners: HashSet<Username>,
+        cards: HashMap<Username, Card>,
+        seats: Seats,
+    }
+    Completed {
+        winner: Username,
+    }
+}
index 3aeb8318f044d9c8a3f0374f6327762e54bd25de..3b9c853a2371d98e4c2eb826848c91e4ab959a4a 100644 (file)
@@ -2,17 +2,36 @@ use std::collections::{BTreeMap, HashSet};
 
 use crate::card::*;
 use crate::game::{Action, UserAction};
+use crate::hands::Hands;
 use crate::seats::Seats;
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct GameState {
     actions: Vec<UserAction>,
+    seats: Seats,
+    players_in_hand: Seats,
+    hands: Hands,
+    trumps: Option<Suit>,
+    dealing_to: Option<String>,
+    dealer: Option<String>,
+    active_player: Option<String>,
+    next_active_player: Option<String>,
+    has_started: bool,
 }
 
 impl GameState {
     pub fn new() -> Self {
         Self {
             actions: Vec::new(),
+            seats: Seats::new(),
+            players_in_hand: Seats::new(),
+            hands: Hands::new(),
+            trumps: None,
+            dealing_to: None,
+            dealer: None,
+            active_player: None,
+            next_active_player: None,
+            has_started: false,
         }
     }
 
@@ -23,6 +42,7 @@ impl GameState {
     pub fn view_for(&self, username: &str) -> Self {
         Self {
             actions: self.actions.iter().map(|action| action.view_for(username)).collect(),
+            ..self.clone()
         }
     }
 
@@ -30,135 +50,126 @@ impl GameState {
         self.actions.iter().skip(from).map(|action| action.view_for(username)).collect()
     }
 
-    pub fn user_has_joined(&self, user: &str) -> bool {
-        let mut logged_in = false;
-        for action in &self.actions {
-            match action {
-                UserAction{ref username, action: Action::Join{..}} if user == username => logged_in = true,
-                UserAction{ref username, action: Action::Leave} if user == username => logged_in = false,
-                _ => continue,
-            }
-        }
-        logged_in
+    pub fn player_has_joined(&self, username: &str) -> bool {
+        self.seats.contains_player(username)
     }
 
     pub fn players(&self) -> HashSet<&str> {
-        let mut players = HashSet::new();
-        for action in &self.actions {
-            match action.action {
-                Action::Join{..} => players.insert(&*action.username),
-                Action::Leave => players.remove(&*action.username),
-                _ => continue,
-            };
-        }
-        players
+        self.seats.player_set()
     }
 
-    pub fn num_players(&self) -> u32 {
-        let mut num_players = 0;
-        for action in &self.actions {
-            match action.action {
-                Action::Join{..} => num_players += 1,
-                Action::Leave => num_players -= 1,
-                _ => continue,
-            }
-        }
-        num_players
+    pub fn players_len(&self) -> u32 {
+        self.seats.players_len() as u32
     }
 
-    pub fn is_dealing_to(&self, username: &str) -> bool {
-        let mut is_dealing_to = None;
-        let mut seats = Seats::new();
-        for action in &self.actions {
-            match &action.action {
-                Action::Join{seat, ..} => seats.add_player(*seat, &action.username),
-                Action::Leave => seats.remove_player(&action.username),
-                Action::NextToDeal => is_dealing_to = seats.player_after(&action.username),
-                Action::ReceiveCard{..} if is_dealing_to.as_ref().map(String::as_str) == Some(&action.username) => is_dealing_to = seats.player_after(&action.username),
-                Action::ReceiveCard{..} => error!("Expected {:?} to be dealt a card, but {:?} received one", is_dealing_to, username),
-                Action::WinHand{..} => is_dealing_to = seats.player_after(&action.username),
-                Action::EndDeal | Action::WinGame => is_dealing_to = None,
-                _ => {},
-            }
-        }
-        is_dealing_to.as_ref().map(String::as_str) == Some(username)
+    pub fn deck(&self) -> HashSet<Card> {
+        self.hands.deck()
     }
 
-    pub fn seat_is_available(&self, seat: u32) -> bool {
-        let mut seats = Seats::new();
-        for action in &self.actions {
-            match &action.action {
-                Action::Join{seat, ..} => seats.add_player(*seat, &action.username),
-                Action::Leave => seats.remove_player(&action.username),
-                _ => {},
-            }
-        }
-        seats.seat_is_available(seat)
+    pub fn dealer(&self) -> Option<String> {
+        self.dealer.clone()
     }
 
-    pub fn is_choosing_next_to_deal(&self) -> bool {
-        for action in &self.actions {
-            if let Action::NextToDeal = &action.action {
-                return false;
-            }
-        }
-        true
+    pub fn dealing_to(&self) -> Option<String> {
+        self.dealing_to.clone()
+    }
+
+    pub fn is_dealing_to(&self, username: &str) -> bool {
+        self.dealing_to.as_ref().map(String::as_str) == Some(username)
+    }
+
+    pub fn seat_is_available(&self, seat: u32) -> bool {
+        self.seats.seat_is_available(seat)
     }
 
     pub fn has_started(&self) -> bool {
-        for action in &self.actions {
-            if let Action::NextToDeal = &action.action {
-                return true;
-            }
-        }
-        false
+        self.has_started
     }
 
     pub fn player_is_active(&self, username: &str) -> bool {
-        let mut dealer = None;
-        let mut active_player = None;
-        let mut next_active_player = None;
-        let mut seats = Seats::new();
-        let mut players_in_hand = Seats::new();
-        for action in &self.actions {
-            println!("processing action: {:?}", action);
-            match &action.action {
-                Action::Join{seat, ..} => seats.add_player(*seat, &action.username),
-                Action::Leave => seats.remove_player(&action.username),
-                Action::NextToDeal => {
-                    players_in_hand = seats.clone();
-                    next_active_player = players_in_hand.player_after(&action.username);
-                    dealer = Some(action.username.clone());
-                }
-                Action::PlayCard{..} | Action::Bet{..} => {
-                    active_player = players_in_hand.player_after(&action.username);
-                }
-                Action::Fold => {
-                    active_player = players_in_hand.player_after(&action.username);
-                    players_in_hand.remove_player(&action.username);
-                }
-                Action::WinHand{..} => active_player = None,
-                Action::EndDeal => active_player = next_active_player.clone(),
-                Action::WinGame => active_player = None,
-                _ => {},
-            }
-            println!("dealer: {:?}", dealer);
-            println!("active_player: {:?}", active_player);
-            println!("next_active_player: {:?}", next_active_player);
-            println!("seats: {:?}", seats);
-        }
-        active_player.as_ref().map(String::as_str) == Some(username)
+        self.active_player.as_ref().map(String::as_str) == Some(username)
     }
 
     pub fn player_has_card(&self, username: &str, card: Card) -> bool {
-        false // TODO
+        self.hands.player_has_card(username, card)
+    }
+
+    pub fn all_players_have_played_a_card(&self) -> bool {
+        self.hands.all_players_have_played_a_card()
     }
 
-    pub fn is_dealing_community_cards(&self) -> bool {
-        false // TODO
+    pub fn trick_winner(&self) -> Option<String> {
+        self.hands.trick_winner(self.trumps)
     }
 
     pub fn take_action(&mut self, action: UserAction) {
+        match &action.action {
+            Action::Join{seat, ..} => self.seats.add_player(*seat, &action.username),
+            Action::Leave | Action::KnockedOut => self.seats.remove_player(&action.username),
+            Action::AddOn{..} => {
+                // TODO
+            }
+            Action::NextToDeal => {
+                self.players_in_hand = self.seats.clone();
+                self.next_active_player = self.players_in_hand.player_after(&action.username);
+                self.dealer = Some(action.username.clone());
+                self.dealing_to = self.seats.player_after(&action.username);
+                self.has_started = true;
+            }
+            Action::ReceiveCard{card: Some(card)} if self.dealing_to.as_ref().map(String::as_str) == Some(&action.username) => {
+                self.dealing_to = self.seats.player_after(&action.username);
+                self.hands.deal_card(action.username.clone(), *card);
+            }
+            Action::ReceiveCard{..} => {
+                error!("Expected {:?} to be dealt a card, but {:?} received one", self.dealing_to, action.username);
+            }
+            Action::CommunityCard{card} => {
+                self.trumps = Some(card.suit);
+                self.hands.deal_community_card(*card);
+            }
+            Action::RevealCard{card} => {
+                self.hands.reveal_card(&action.username, *card);
+            }
+            Action::ChooseTrumps{suit} => {
+                self.trumps = Some(*suit);
+            }
+            Action::PlayCard{card} => {
+                self.active_player = self.players_in_hand.player_after(&action.username);
+                self.hands.play_card(action.username.clone(), *card);
+            }
+            Action::Bet{..} => {
+                self.active_player = self.players_in_hand.player_after(&action.username);
+            }
+            Action::Fold => {
+                self.active_player = self.players_in_hand.player_after(&action.username);
+                self.players_in_hand.remove_player(&action.username);
+            }
+            Action::WinTrick => {
+                self.active_player = Some(action.username.clone());
+                self.hands.clear_trick();
+            }
+            Action::WinHand{..} => {
+                self.active_player = None;
+                self.dealing_to = self.seats.player_after(&action.username);
+                if let Some(dealer) = self.dealer.take() {
+                    self.dealer = self.seats.player_after(&dealer);
+                    if let Some(dealer) = &self.dealer {
+                        self.dealing_to = self.seats.player_after(&dealer);
+                    }
+                }
+                self.hands.clear();
+            }
+            Action::EndDeal => {
+                self.active_player = self.next_active_player.clone();
+                self.dealing_to = None;
+            }
+            Action::WinGame => {
+                self.active_player = None;
+                self.dealing_to = None;
+                self.hands.clear();
+            }
+            Action::Message{..} => {}
+        }
         self.actions.push(action);
     }
 }
@@ -168,21 +179,21 @@ mod tests {
     use super::*;
     
     #[test]
-    fn user_has_joined() {
+    fn player_has_joined() {
         let mut state = GameState::new();
         state.take_action(UserAction{username: "user".to_string(), action: Action::Join{seat: 0, chips: 10000}});
-        assert!(state.user_has_joined("user"));
+        assert!(state.player_has_joined("user"));
     }
     
     #[test]
-    fn num_players() {
+    fn players_len() {
         let mut state = GameState::new();
         state.take_action(UserAction{username: "user".to_string(), action: Action::Join{seat: 0, chips: 10000}});
-        assert_eq!(1, state.num_players());
+        assert_eq!(1, state.players_len());
         state.take_action(UserAction{username: "user2".to_string(), action: Action::Join{seat: 1, chips: 10000}});
-        assert_eq!(2, state.num_players());
+        assert_eq!(2, state.players_len());
         state.take_action(UserAction{username: "user".to_string(), action: Action::Leave});
-        assert_eq!(1, state.num_players());
+        assert_eq!(1, state.players_len());
     }
 
     #[test]
@@ -226,7 +237,7 @@ mod tests {
     }
 
     #[test]
-    fn player_is_active() {
+    fn player_is_active_hold_em() {
         let mut state = GameState::new();
         state.take_action(UserAction{username: "user1".to_string(), action: Action::Join{seat: 0, chips: 10000}});
         state.take_action(UserAction{username: "user2".to_string(), action: Action::Join{seat: 1, chips: 10000}});
@@ -255,4 +266,70 @@ mod tests {
         state.take_action(UserAction{username: "user1".to_string(), action: Action::CommunityCard{card: KING_OF_CLUBS}});
         state.take_action(UserAction{username: "user1".to_string(), action: Action::CommunityCard{card: THREE_OF_HEARTS}});
     }
+
+    #[test]
+    fn player_is_active_whist() {
+        let mut state = GameState::new();
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::Join{seat: 0, chips: 0}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::Join{seat: 1, chips: 0}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::Join{seat: 2, chips: 0}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::NextToDeal});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(TWO_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(TWO_OF_DIAMONDS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(TWO_OF_HEARTS)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(TWO_OF_SPADES)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(THREE_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(THREE_OF_HEARTS)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(THREE_OF_DIAMONDS)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(THREE_OF_SPADES)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(FOUR_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(FOUR_OF_DIAMONDS)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(FOUR_OF_HEARTS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(FOUR_OF_SPADES)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(FIVE_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(FIVE_OF_DIAMONDS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(FIVE_OF_HEARTS)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(FIVE_OF_SPADES)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(SIX_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(SIX_OF_DIAMONDS)}});
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::ReceiveCard{card: Some(SIX_OF_HEARTS)}});
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::ReceiveCard{card: Some(SIX_OF_SPADES)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::ReceiveCard{card: Some(SEVEN_OF_CLUBS)}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::CommunityCard{card: SEVEN_OF_DIAMONDS}});
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::EndDeal});
+        assert!(state.player_is_active("user2"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::PlayCard{card: FIVE_OF_CLUBS}});
+        assert!(state.player_is_active("user3"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::PlayCard{card: SIX_OF_CLUBS}});
+        assert!(state.player_is_active("user1"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::PlayCard{card: SEVEN_OF_CLUBS}});
+        assert!(state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::WinTrick});
+        assert!(state.player_is_active("user1"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::PlayCard{card: FIVE_OF_HEARTS}});
+        assert!(state.player_is_active("user2"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::PlayCard{card: SIX_OF_HEARTS}});
+        assert!(state.player_is_active("user3"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::PlayCard{card: FOUR_OF_HEARTS}});
+        assert!(state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::WinTrick});
+        assert!(state.player_is_active("user3"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::PlayCard{card: SIX_OF_SPADES}});
+        assert!(state.player_is_active("user1"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user1".to_string(), action: Action::PlayCard{card: FOUR_OF_SPADES}});
+        assert!(state.player_is_active("user2"));
+        assert!(!state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user2".to_string(), action: Action::PlayCard{card: TWO_OF_SPADES}});
+        assert!(state.all_players_have_played_a_card());
+        state.take_action(UserAction{username: "user3".to_string(), action: Action::WinTrick});
+        assert!(state.player_is_active("user3"));
+    }
 }
diff --git a/src/hands.rs b/src/hands.rs
new file mode 100644 (file)
index 0000000..c4b95bb
--- /dev/null
@@ -0,0 +1,96 @@
+use std::collections::{HashMap, HashSet};
+
+use itertools::Itertools;
+
+use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Hands {
+    hands: HashMap<String, HashSet<Card>>,
+    community: Vec<Card>,
+    trick: Vec<(String, Card)>,
+    deck: HashSet<Card>,
+}
+
+impl Hands {
+    pub fn new() -> Hands {
+        Hands {
+            hands: HashMap::new(),
+            community: Vec::new(),
+            trick: Vec::new(),
+            deck: FIFTY_TWO_CARD_DECK.iter().cloned().collect(),
+        }
+    }
+
+    pub fn clear(&mut self) {
+        self.hands.clear();
+        self.community.clear();
+        self.trick.clear();
+        self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
+    }
+
+    pub fn clear_trick(&mut self) {
+        self.trick.clear();
+    }
+
+    pub fn trick_winner(&self, trumps: Option<Suit>) -> Option<String> {
+        let mut winner = None;
+        for (player, card) in &self.trick {
+            match winner {
+                None => winner = Some((player, card)),
+                Some((_, best)) if
+                    Some(card.suit) == trumps && (card.rank > best.rank || Some(best.suit) != trumps) ||
+                    card.suit == best.suit && card.rank > best.rank
+                        => winner = Some((player, card)),
+                Some((_, _)) => {},
+            }
+        }
+        winner.map(|(player, _)| (*player).clone())
+    }
+
+    pub fn all_players_have_played_a_card(&self) -> bool {
+        let all_players: HashSet<_> = self.hands.keys().collect();
+        let players_that_have_played_a_card: HashSet<_> = self.trick.iter().map(|(player, _)| player).collect();
+        all_players == players_that_have_played_a_card
+    }
+
+    pub fn play_card(&mut self, username: String, card: Card) {
+        if let Some(set) = self.hands.get_mut(&username) {
+            if set.remove(&card) {
+                self.trick.push((username, card));
+            } else {
+                error!("Tried to remove {} from {}'s hand - had [{}]", card, username, set.iter().format(", "));
+            }
+        } else {
+            error!("Tried to remove {} from {}'s hand - had [{}]", card, username, self.hands.keys().format(", "));
+        }
+    }
+
+    pub fn reveal_card(&mut self, username: &str, card: Card) {
+        // TODO
+    }
+
+    pub fn deck(&self) -> HashSet<Card> {
+        self.deck.clone()
+    }
+
+    pub fn deal_card(&mut self, username: String, card: Card) {
+        if !self.deck.remove(&card) {
+            error!("Tried to deal card {} but it was not present in deck: [{}]", card, self.deck.iter().format(", "));
+        }
+        let set = self.hands.entry(username).or_insert_with(HashSet::new);
+        set.insert(card);
+    }
+
+    pub fn deal_community_card(&mut self, card: Card) {
+        self.community.push(card);
+    }
+
+    pub fn player_has_card(&self, username: &str, card: Card) -> bool {
+        if let Some(hand) = self.hands.get(username) {
+            hand.contains(&card)
+        } else {
+            false
+        }
+    }
+}
index 4eb2784e21ba23ba3dd6e73a919a5565935233d5..1e0b4f0777b4a0016c1dfaf0478151272bc4e3a5 100644 (file)
@@ -5,6 +5,7 @@ use std::collections::HashSet;
 use std::convert::TryFrom;
 use std::mem::swap;
 
+use async_std::task::spawn;
 use futures::{channel::mpsc::{channel, Receiver}, future::{Either, select}, select, FutureExt, SinkExt, StreamExt, pin_mut, stream::{self, FuturesUnordered}};
 use redis::{aio::PubSub, Client, Msg};
 use signal_hook::consts::signal::*;
@@ -18,14 +19,20 @@ mod api;
 mod auth;
 mod card;
 mod client;
+mod dealer;
 mod game;
+mod games;
 mod gamestate;
+mod hands;
 mod seats;
 mod server;
+mod username;
 
 use crate::api::ServerMessage;
-use crate::server::{ClientInterestSender, Server, ServerState};
 use crate::client::{ClientInterest, ConnectionState};
+use crate::dealer::Dealer;
+use crate::game::GameList;
+use crate::server::{ClientInterestSender, Server, ServerState};
 
 pub async fn handle_client_interest(mut connection: PubSub, mut new_clients: Receiver<ClientInterestSender>) -> Result<(), std::io::Error> {
     #[derive(Debug)]
@@ -79,7 +86,7 @@ pub async fn handle_client_interest(mut connection: PubSub, mut new_clients: Rec
             action
         } {
             Action::AddClient{sender} => {
-                debug!("handle_client_interest: Action::AddClient {{ sender: {:?} }}", sender);
+                debug!("handle_client_interest: Action::AddClient {{ clients[{}] }}", clients.len());
                 clients.push(Client{sender, interests: HashSet::new()});
             }
             Action::RegisterInterest{index, mut client_interests} => {
@@ -102,7 +109,7 @@ pub async fn handle_client_interest(mut connection: PubSub, mut new_clients: Rec
                 let client = &mut clients[index];
                 let sender = &mut client.sender;
                 for interest in &client.interests - &client_interests {
-                    debug!("handle_client_interest: Sending initial interest for new interest {:?} to {:?}", interest, sender);
+                    debug!("handle_client_interest: Sending initial interest for new interest {:?} to clients[{}]", interest, index);
                     if let Err(err) = sender.interest.send(interest.clone()).await {
                         error!("handle_client_interest: Send failed: {}", err);
                     }
@@ -147,6 +154,33 @@ pub async fn handle_client_interest(mut connection: PubSub, mut new_clients: Rec
     }
 }
 
+async fn handle_new_games(server: Server) -> Result<(), std::io::Error> {
+    let (mut server_state, mut update_stream) = server.new_state().await;
+    let mut interests = HashSet::new();
+    interests.insert(ClientInterest::GameList);
+    server_state.register_interests(interests).await;
+    let mut game_list = GameList::new("".to_string());
+    loop {
+        let games_len = game_list.games_len();
+        match server_state.update_game_list(&mut game_list).await {
+            Ok(()) => {
+                for summary in game_list.update(games_len) {
+                    let (server_state, update_stream) = server.new_state().await;
+                    if let Ok(dealer) = Dealer::new(server_state, summary.id()).await {
+                        spawn(dealer.start(update_stream));
+                    }
+                }
+            }
+            Err(err) => {
+                error!("Failed to update game list: {}", err);
+                return Err(std::io::Error::new(std::io::ErrorKind::Other, err));
+            }
+        }
+        if let Some(ClientInterest::GameList) = update_stream.next().await { continue; }
+        return Ok(());
+    }
+}
+
 pub async fn handle_websocket_request(request: Request<Server>, stream: WebSocketConnection) -> Result<(), Error> {
     let (server_state, update_stream): (ServerState, Receiver<_>) = request.state().new_state().await;
     let mut client = ConnectionState::new(server_state);
@@ -226,6 +260,7 @@ async fn main() -> Result<(), Error> {
     let signal_handler = handle_signals(signals);
 
     let handle_client_interest = handle_client_interest(pubsub, register_update_stream_rx);
+    let handle_new_games = handle_new_games(app.state().clone());
 
     /*let listener = TlsListener::build()
         .addrs("localhost:4433")
@@ -234,9 +269,9 @@ async fn main() -> Result<(), Error> {
 
     let app = app.listen(listener);*/
     let app = app.listen("0.0.0.0:8080");
-    pin_mut!(app, handle_client_interest, signal_handler);
+    pin_mut!(app, handle_client_interest, handle_new_games, signal_handler);
 
-    select(select(app, handle_client_interest).map(|f| f.factor_first().0), signal_handler).await.factor_first().0?;
+    select(select(app, select(handle_client_interest, handle_new_games).map(|f| f.factor_first().0)).map(|f| f.factor_first().0), signal_handler).await.factor_first().0?;
 
     info!("Pokerwave shut down gracefully.");
 
index d32cfa81df05555ba08ba0a3ff5adebe71740266..bb861b39f9fd5d5d12d5a39bcef5917f153d5c9c 100644 (file)
@@ -1,6 +1,6 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Seats {
     players: BTreeMap<u32, String>,
 }
@@ -41,6 +41,19 @@ impl Seats {
         None
     }
 
+    pub fn contains_player(&self, username: &str) -> bool {
+        for (_, player) in &self.players {
+            if player == username {
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn player_set(&self) -> HashSet<&str> {
+        self.players.iter().map(|(_, player)| &**player).collect()
+    }
+
     pub fn seat_is_available(&self, seat: u32) -> bool {
         self.players.get(&seat).is_none()
     }
index 588644872f1ad4fee50c7b16de64f058e76074b0..5e4ecbe39b912b4bfca1d17b0470d7c23d59b446 100644 (file)
@@ -7,7 +7,9 @@ use serde::{Serialize, Deserialize};
 
 use crate::auth::Auth;
 use crate::client::ClientInterest;
-use crate::game::{Game, GameList, GameSettings, GameSummary, UserAction};
+use crate::game::{GameList, GameSettings, GameSummary, UserAction};
+use crate::games::ValidatedUserAction;
+use crate::username::Username;
 
 #[derive(Clone)]
 pub struct Server {
@@ -39,6 +41,7 @@ fn client_interest_channel() -> (ClientInterestSender, ClientInterestReceiver) {
 
 pub enum ClientInterestFromMsgError {
     InvalidChannelName{channel_name: String},
+    UsernameParseError(&'static str),
 }
 
 impl TryFrom<Msg> for ClientInterest {
@@ -48,7 +51,8 @@ impl TryFrom<Msg> for ClientInterest {
         if channel_name == "__keyspace@0__:games" {
             Ok(ClientInterest::GameList)
         } else if let Some(username) = channel_name.strip_prefix("__keyspace@0__:user:") {
-            Ok(ClientInterest::User{username: username.to_string()})
+            username.parse().map_err(ClientInterestFromMsgError::UsernameParseError)
+                .map(|username| ClientInterest::User{username})
         } else if let Some(Ok(id)) = channel_name.strip_prefix("__keyspace@0__:game:").map(str::parse) {
             Ok(ClientInterest::Game{id})
         } else {
@@ -106,7 +110,7 @@ pub struct ServerState {
     take_action_script: Script,
 }
 
-fn user_key(username: &str) -> String {
+fn user_key(username: Username) -> String {
     format!("user:{}", username)
 }
 
@@ -115,7 +119,7 @@ fn game_key(id: u32) -> String {
 }
 
 impl ServerState {
-    pub async fn create_user(&mut self, username: &str, auth: Auth, nickname: &str) -> RedisResult<()> {
+    pub async fn create_user(&mut self, username: Username, auth: Auth, nickname: &str) -> RedisResult<()> {
         let key = user_key(username);
         if self.redis.hset_nx::<_, _, _, i32>(&key, "auth", AsJson(auth)).await? == 0 {
             return Err(RedisError::from((ErrorKind::ResponseError, "User already exists")));
@@ -123,22 +127,22 @@ impl ServerState {
         self.redis.hset(&key, "nickname", nickname).await
     }
 
-    pub async fn set_user_auth(&mut self, username: &str, auth: Auth) -> RedisResult<()> {
+    pub async fn set_user_auth(&mut self, username: Username, auth: Auth) -> RedisResult<()> {
         let key = user_key(username);
         self.redis.hset(key, "auth", AsJson(auth)).await
     }
 
-    async fn get_user_auth(&mut self, username: &str) -> Option<Auth> {
+    async fn get_user_auth(&mut self, username: Username) -> Option<Auth> {
         let key = user_key(username);
         self.redis.hget(key, "auth").await.ok().map(AsJson::get)
     }
 
-    pub async fn set_user_nickname(&mut self, username: &str, nickname: &str) -> RedisResult<()> {
+    pub async fn set_user_nickname(&mut self, username: Username, nickname: &str) -> RedisResult<()> {
         let key = user_key(username);
         self.redis.hset(key, "nickname", nickname).await
     }
 
-    pub async fn verify(&mut self, username: &str, challenge: &str, signature: &str) -> bool {
+    pub async fn verify(&mut self, username: Username, challenge: &str, signature: &str) -> bool {
         match self.get_user_auth(username).await {
             None => false,
             Some(auth) => auth.verify(challenge, signature),
@@ -164,27 +168,18 @@ impl ServerState {
         Ok(())
     }
 
-    pub async fn update_game_state(&mut self, game: &mut Game) -> RedisResult<()> {
-        const GAME_ACTION_BLOCK_SIZE: isize = 1024;
-        let key = game_key(game.id());
-        for i in (game.actions_len() as isize..).step_by(GAME_ACTION_BLOCK_SIZE as usize) {
-            let actions: Vec<AsJson<UserAction>> = self.redis.lrange(&key, i, i + GAME_ACTION_BLOCK_SIZE).await?;
-            if actions.is_empty() { break; }
-            for AsJson(action) in actions {
-                game.take_action(action).map_err(|err| RedisError::from((ErrorKind::ResponseError, "Invalid action", err.to_string())))?;
-            }
-        }
-        Ok(())
+    pub async fn game_state(&mut self, id: u32, from: usize) -> RedisResult<Vec<ValidatedUserAction>> {
+        let key = game_key(id);
+        let actions: Vec<AsJson<ValidatedUserAction>> = self.redis.lrange(&key, from as isize, -1).await?;
+        Ok(actions.into_iter().map(AsJson::get).collect())
     }
 
-    pub async fn get_game(&mut self, id: u32) -> RedisResult<Game> {
+    pub async fn game_summary(&mut self, id: u32) -> RedisResult<GameSummary> {
         let settings = self.redis.lindex("games", id as isize).await.map(AsJson::get)?;
-        let mut game = Game::new(id, settings);
-        self.update_game_state(&mut game).await?;
-        Ok(game)
+        Ok(GameSummary::new(id, settings))
     }
 
-    pub async fn take_action(&mut self, id: u32, len: usize, action: &UserAction) -> RedisResult<ActionStatus> {
+    pub async fn take_action(&mut self, id: u32, len: usize, action: &ValidatedUserAction) -> RedisResult<ActionStatus> {
         let key = game_key(id);
         debug!("take_action: EVAL {{TAKE_ACTION_LUA_SCRIPT}} 1 {} {} {:?}", key, len, action);
         self.take_action_script.key(key).arg(len).arg(AsJson(action)).invoke_async(&mut self.redis).await
diff --git a/src/username.rs b/src/username.rs
new file mode 100644 (file)
index 0000000..4313ad5
--- /dev/null
@@ -0,0 +1,109 @@
+use std::fmt::{self, Display, Formatter};
+use std::str::{self, FromStr, Utf8Error};
+
+use serde::{Serialize, Deserialize, Serializer, Deserializer, ser::Error, de::{self, Visitor}};
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(align(8))]
+pub struct Username {
+    username: [u8; Self::MAX_LENGTH],
+}
+
+impl Username {
+    const MIN_LENGTH: usize = 1;
+    const MAX_LENGTH: usize = 32;
+
+    fn as_str(&self) -> Result<&str, Utf8Error> {
+        str::from_utf8(&self.username)
+            .map(|str| str.trim_end_matches('\0'))
+    }
+}
+
+pub const DEALER: Username = Username {
+    username: [
+        b'd', b'e', b'a', b'l', b'e', b'r', 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0,
+    ],
+};
+
+impl Display for Username {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.as_str() {
+            Ok(str) => f.write_str(str),
+            Err(_) => f.write_str(String::from_utf8_lossy(&self.username).trim_end_matches('\0')),
+        }
+    }
+}
+
+impl FromStr for Username {
+    type Err = &'static str;
+
+    fn from_str(str: &str) -> Result<Self, Self::Err> {
+        let bytes = str.as_bytes();
+        let len = bytes.len();
+        if len < Username::MIN_LENGTH {
+            Err("string was too short")
+        } else if len > Username::MAX_LENGTH {
+            Err("string was too long")
+        } else if bytes.iter().any(|b| !b.is_ascii_alphanumeric() && *b != b'_') {
+            Err("string contained values not in [A-Za-z0-9_]")
+        } else {
+            let mut username = [0; 32];
+            username[0..len].copy_from_slice(bytes);
+            Ok(Username{username})
+        }
+    }
+}
+
+impl Serialize for Username {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where S: Serializer
+    {
+        match self.as_str() {
+            Ok(str) => serializer.serialize_str(str),
+            Err(err) => Err(S::Error::custom(err)),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Username {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+        where D: Deserializer<'de>
+    {
+        struct StrVisitor;
+        impl Visitor<'_> for StrVisitor {
+            type Value = Username;
+            fn expecting(&self, f: &mut Formatter) -> fmt::Result {
+                write!(f, "a string containing {}-{} characters [A-Za-z0-9_]", Username::MIN_LENGTH, Username::MAX_LENGTH)
+            }
+
+            fn visit_str<E>(self, str: &str) -> Result<Username, E>
+                where E: de::Error
+            {
+                str.parse().map_err(E::custom)
+            }
+        }
+        deserializer.deserialize_str(StrVisitor)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn deserialize_username() {
+        let username: Username = serde_json::from_str(r#""user1234""#).unwrap();
+        assert_eq!(username, Username{username: [b'u', b's', b'e', b'r', b'1', b'2', b'3', b'4', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]});
+        assert!(serde_json::from_str::<Username>(r#""\0""#).is_err());
+        assert!(serde_json::from_str::<Username>(r#""this_is_a_very_long_username_that_fails_to_parse""#).is_err());
+    }
+
+    #[test]
+    fn username_to_string() {
+        let username: Username = serde_json::from_str(r#""user1234""#).unwrap();
+        assert_eq!("user1234", username.to_string());
+    }
+}