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"
"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"
"async-std",
"env_logger",
"futures",
+ "itertools",
"log",
"rand 0.8.3",
+ "rand_chacha 0.3.0",
"redis",
"serde",
"serde_derive",
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"
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+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";
+}
+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;
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);
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");
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: ""});
});
}
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;
}
}
}
}
+ 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;
@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%;
--- /dev/null
+svg {
+ width: 100%;
+ height: 100%;
+}
+
+.my-card {
+ transform: none;
+ transition: transform 0.5s;
+}
+
+.my-card:hover {
+ transform: translateY(-20px);
+}
--- /dev/null
+<!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>
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 {
#[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 },
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 },
}
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,
}
}
-#[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,
}
}
-#[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,
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,
#[derive(Debug, Clone)]
pub enum ClientState {
Connected,
- LoginAuthIssued { username: String, challenge: String },
+ LoginAuthIssued { username: Username, challenge: String },
LoggedIn {
- username: String,
+ username: Username,
state: LoggedInState,
},
}
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 {
_ => 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(),
}
}
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 => {},
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});
}
}
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 {
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(()) => {
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()},
}
--- /dev/null
+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)
+ }
+ }
+ }
+}
WinGame,
Message { message: String },
Leave,
+ KnockedOut,
}
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)]
NotAuthorised,
AlreadyJoined,
GameHasStarted,
+ SeatNotAvailable,
NoSeatAvailable,
OutOfTurn,
CardNotPlayable,
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"),
pub fn new(id: u32, settings: GameSettings) -> Self {
Self{id, settings}
}
+
+ pub fn id(&self) -> u32 {
+ self.id
+ }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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(),
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),
}
}
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(())
}
}
--- /dev/null
+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,
+ }
+ }
+}
--- /dev/null
+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!()
+ }
+}
--- /dev/null
+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 {
+ }
+}
--- /dev/null
+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,
+ }
+}
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,
}
}
pub fn view_for(&self, username: &str) -> Self {
Self {
actions: self.actions.iter().map(|action| action.view_for(username)).collect(),
+ ..self.clone()
}
}
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);
}
}
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]
}
#[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}});
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"));
+ }
}
--- /dev/null
+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
+ }
+ }
+}
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::*;
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)]
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} => {
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);
}
}
}
+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);
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")
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.");
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Seats {
players: BTreeMap<u32, String>,
}
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()
}
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 {
pub enum ClientInterestFromMsgError {
InvalidChannelName{channel_name: String},
+ UsernameParseError(&'static str),
}
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 {
take_action_script: Script,
}
-fn user_key(username: &str) -> String {
+fn user_key(username: Username) -> String {
format!("user:{}", username)
}
}
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")));
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),
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
--- /dev/null
+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());
+ }
+}