"async-io",
"async-lock",
"async-process",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils 0.8.3",
"futures-channel",
"futures-core",
"futures-io",
[[package]]
name = "crossbeam-utils"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3"
+checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"lazy_static",
- "loom",
]
[[package]]
"slab",
]
-[[package]]
-name = "generator"
-version = "0.6.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9fed24fd1e18827652b4d55652899a1e9da8e54d91624dc3437a5bc3a9f9a9c"
-dependencies = [
- "cc",
- "libc",
- "log",
- "rustversion",
- "winapi",
-]
-
[[package]]
name = "generic-array"
version = "0.14.4"
"libc",
]
+[[package]]
+name = "hex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "hkdf"
version = "0.10.0"
[[package]]
name = "js-sys"
-version = "0.3.47"
+version = "0.3.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65"
+checksum = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78"
dependencies = [
"wasm-bindgen",
]
"value-bag",
]
-[[package]]
-name = "loom"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d44c73b4636e497b4917eb21c33539efa3816741a2d3ff26c6316f1b529481a4"
-dependencies = [
- "cfg-if 1.0.0",
- "generator",
- "scoped-tls",
-]
-
[[package]]
name = "matches"
version = "0.1.8"
[[package]]
name = "once_cell"
-version = "1.6.0"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd"
+checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a"
[[package]]
name = "opaque-debug"
"async-std",
"env_logger",
"futures",
+ "getrandom 0.2.2",
+ "hex",
"itertools",
"log",
"rand 0.8.3",
[[package]]
name = "redis"
-version = "0.19.0"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a6ddfecac9391fed21cce10e83c65fa4abafd77df05c98b1c647c65374ce9b3"
+checksum = "eeb8f8d059ead7805e171fc22de8348a3d611c0f985aaa4f5cf6c0dfc7645407"
dependencies = [
"async-std",
"async-trait",
"webpki",
]
-[[package]]
-name = "rustversion"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd"
-
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
-[[package]]
-name = "scoped-tls"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
-
[[package]]
name = "sct"
version = "0.6.0"
[[package]]
name = "serde_json"
-version = "1.0.62"
+version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
[[package]]
name = "wasm-bindgen"
-version = "0.2.70"
+version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be"
+checksum = "7ee1280240b7c461d6a0071313e08f34a60b0365f14260362e5a2b17d1d31aa7"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.70"
+version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7"
+checksum = "5b7d8b6942b8bb3a9b0e73fc79b98095a27de6fa247615e59d096754a3bc2aa8"
dependencies = [
"bumpalo",
"lazy_static",
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.20"
+version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94"
+checksum = "8e67a5806118af01f0d9045915676b22aaebecf4178ae7021bc171dab0b897ab"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.70"
+version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c"
+checksum = "e5ac38da8ef716661f0f36c0d8320b89028efe10c7c0afde65baffb496ce0d3b"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.70"
+version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385"
+checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e"
dependencies = [
"proc-macro2",
"quote",
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.70"
+version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
+checksum = "7d6f8ec44822dd71f5f221a5847fb34acd9060535c1211b70a05844c0f6383b1"
[[package]]
name = "web-sys"
-version = "0.3.47"
+version = "0.3.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3"
+checksum = "ec600b26223b2948cedfde2a0aa6756dcf1fef616f43d7b3097aaf53a6c4d92b"
dependencies = [
"js-sys",
"wasm-bindgen",
env_logger = "0.8"
log = "0.4"
futures = "0.3"
+getrandom = "0.2"
+hex = { version = "0.4", features = ["serde"] }
itertools = "0.10"
rand = "0.8"
rand_chacha = "0.3"
-redis = { version = "0.19", features = ["async-std-comp"] }
+redis = { version = "0.20", features = ["async-std-comp"] }
serde = "1"
serde_derive = "1"
serde_json = "1"
--- /dev/null
+const svgns = "http://www.w3.org/2000/svg";
+
+import { card_href, suit_href } from "./card.js";
+
+export class TexasHoldEm {
+ constructor(container, summary, actions, username, send) {
+ this.container = container;
+ this.summary = summary;
+ this.actions = actions;
+ this.username = username;
+ this.send = send;
+ this.seats = new Map();
+ this.stacks = new Map();
+ this.hands = new Map();
+ this.bets = new Map();
+ this.pot = 0;
+ this.community = [];
+ this.user_icons = new Map();
+ this.small_blind = this.summary.settings.small_blind;
+ this.big_blind = this.small_blind * 2;
+
+ 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);
+
+ const controls = document.createElementNS(svgns, "rect");
+ controls.setAttribute("x", "310");
+ controls.setAttribute("y", "435");
+ controls.setAttribute("width", "185");
+ controls.setAttribute("height", "60");
+ controls.setAttribute("rx", "15");
+ controls.classList.add("controls");
+ this.svg.append(controls);
+
+ this.fold_control = document.createElementNS(svgns, "rect");
+ this.fold_control.setAttribute("x", "315");
+ this.fold_control.setAttribute("y", "440");
+ this.fold_control.setAttribute("width", "55");
+ this.fold_control.setAttribute("height", "50");
+ this.fold_control.setAttribute("rx", "10");
+ this.fold_control.onclick = () => this.send({type: "TakeAction", action: {action: "Fold"}});
+ this.fold_control.classList.add("fold-control");
+ this.svg.append(this.fold_control);
+
+ const fold_control_label = document.createElementNS(svgns, "text");
+ const fold_control_text = document.createTextNode("Fold");
+ fold_control_label.append(fold_control_text);
+ fold_control_label.setAttribute("x", "325");
+ fold_control_label.setAttribute("y", "470");
+ fold_control_label.classList.add("fold-control-label");
+ this.svg.append(fold_control_label);
+
+ this.call_control = document.createElementNS(svgns, "rect");
+ this.call_control.setAttribute("x", "375");
+ this.call_control.setAttribute("y", "440");
+ this.call_control.setAttribute("width", "55");
+ this.call_control.setAttribute("height", "50");
+ this.call_control.setAttribute("rx", "10");
+ this.call_control.onclick = () => this.send({type: "TakeAction", action: {action: "Bet", chips: this.chips_to_call()}});
+ this.call_control.classList.add("call-control");
+ this.svg.append(this.call_control);
+
+ const call_control_label = document.createElementNS(svgns, "text");
+ const call_control_text = document.createTextNode("Call");
+ call_control_label.append(call_control_text);
+ call_control_label.setAttribute("x", "385");
+ call_control_label.setAttribute("y", "470");
+ call_control_label.classList.add("call-control-label");
+ this.svg.append(call_control_label);
+
+ this.bet_control = document.createElementNS(svgns, "rect");
+ this.bet_control.setAttribute("x", "435");
+ this.bet_control.setAttribute("y", "440");
+ this.bet_control.setAttribute("width", "55");
+ this.bet_control.setAttribute("height", "50");
+ this.bet_control.setAttribute("rx", "10");
+ this.bet_control.onclick = () => {
+ const chips = prompt("Chips to bet", this.chips_to_call() + this.min_raise());
+ if (chips !== null && !isNaN(Number(chips))) {
+ this.send({type: "TakeAction", action: {action: "Bet", chips: Number(chips)}});
+ }
+ }
+ this.bet_control.classList.add("bet-control");
+ this.svg.append(this.bet_control);
+
+ const bet_control_label = document.createElementNS(svgns, "text");
+ const bet_control_text = document.createTextNode("Bet");
+ bet_control_label.append(bet_control_text);
+ bet_control_label.setAttribute("x", "445");
+ bet_control_label.setAttribute("y", "470");
+ bet_control_label.classList.add("bet-control-label");
+ this.svg.append(bet_control_label);
+
+ this.container.append(this.svg);
+ for (const user_action of this.actions) {
+ this.take_action(user_action);
+ }
+
+ if (!this.seats.has(this.username)) {
+ this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: this.summary.settings.starting_stack}});
+ }
+ }
+
+ redraw_players() {
+ this.fold_control.classList.toggle("active", this.active === this.username);
+ this.call_control.classList.toggle("active", this.active === this.username);
+ this.bet_control.classList.toggle("active", this.active === this.username);
+ for (const [username, [user, stack, active, bet]] of this.user_icons) {
+ if (!this.seats.has(username)) {
+ this.svg.removeChild(user);
+ this.svg.removeChild(stack);
+ this.svg.removeChild(active);
+ this.svg.removeChild(bet);
+ this.user_icons.delete(username);
+ } else {
+ const seat = this.seats.get(username);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const x = 240 - 180 * Math.sin(angle);
+ const y = 240 + 120 * Math.cos(angle) + 60 * (Math.sign(Math.cos(angle)) || 1);
+ user.setAttribute("x", x);
+ user.setAttribute("y", y);
+ stack.childNodes[0].nodeValue = this.stacks.get(username);
+ stack.setAttribute("x", x);
+ stack.setAttribute("y", y + 20);
+ active.classList.toggle("active", this.active === username);
+ active.setAttribute("cx", x - 10);
+ active.setAttribute("cy", y - 5);
+ bet.childNodes[0].nodeValue = this.bets.get(username) || "";
+ bet.setAttribute("x", 240 - 120 * Math.sin(angle));
+ bet.setAttribute("y", 250 + 70 * Math.cos(angle));
+ }
+ }
+ for (const [username, seat] of this.seats) {
+ if (!this.user_icons.has(username)) {
+ const user = document.createElementNS(svgns, "text");
+ const text = document.createTextNode(username);
+ user.append(text);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const x = 240 - 180 * Math.sin(angle);
+ const y = 240 + 120 * Math.cos(angle) + 60 * (Math.sign(Math.cos(angle)) || 1);
+ user.setAttribute("x", x);
+ user.setAttribute("y", y);
+ this.svg.append(user);
+
+ const stack = document.createElementNS(svgns, "text");
+ const stack_text = document.createTextNode(this.stacks.get(username));
+ stack.append(stack_text);
+ stack.setAttribute("x", x);
+ stack.setAttribute("y", y + 20);
+ this.svg.append(stack);
+
+ const active = document.createElementNS(svgns, "circle");
+ active.setAttribute("cx", x - 10);
+ active.setAttribute("cy", y - 5);
+ active.setAttribute("r", 5);
+ active.classList.add("active-indicator");
+ active.classList.toggle("active", this.active === username);
+ this.svg.append(active);
+
+ const bet = document.createElementNS(svgns, "text");
+ const bet_text = document.createTextNode(this.bets.get(username) || "");
+ bet.append(bet_text);
+ bet.setAttribute("x", 240 - 120 * Math.sin(angle));
+ bet.setAttribute("y", 250 + 70 * Math.cos(angle));
+ this.svg.append(bet);
+
+ this.user_icons.set(username, [user, stack, active, bet]);
+ }
+ }
+ }
+
+ redraw_cards() {
+ for (const [username, cards] of this.hands) {
+ const seat = this.seats.get(username);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const offset = cards.length * 10;
+ let x = 227.5 + offset - 180 * Math.sin(angle);
+ const y = 210 + 120 * Math.cos(angle);
+ for (const {card, image} of cards) {
+ image.setAttribute("x", x);
+ image.setAttribute("y", y);
+ x -= 20;
+ }
+ }
+ let x = 120.0;
+ for (const {card, image} of this.community) {
+ const y = 210;
+ image.setAttribute("x", x);
+ image.setAttribute("y", y);
+ x += 50;
+ }
+ }
+
+ card_image(username, card) {
+ const image = document.createElementNS(svgns, "image");
+ image.setAttribute("width", "45");
+ image.setAttribute("height", "70");
+ image.setAttribute("href", card_href(card));
+ this.svg.append(image);
+ return image;
+ }
+
+ player_after(username, is_playing) {
+ const in_seat = this.seats.get(username);
+ let [player_after, player_after_seat] = [null, null];
+ for (const [username, seat] of this.seats) {
+ if (is_playing(username) && seat > in_seat && (player_after_seat === null || seat < player_after_seat)) {
+ player_after = username;
+ player_after_seat = seat;
+ }
+ }
+ if (player_after_seat === null) for (const [username, seat] of this.seats) {
+ if (is_playing(username) && (player_after_seat === null || seat < player_after_seat)) {
+ player_after = username;
+ player_after_seat = seat;
+ }
+ }
+ return player_after;
+ }
+
+ chips_to_call() {
+ const max = Math.max(...this.bets.values());
+ const bet = this.bets.get(this.username) || 0;
+ if (max >= 0) {
+ return max - bet;
+ } else {
+ return 0;
+ }
+ }
+
+ min_raise() {
+ if (this.community.length === 0) {
+ return Math.max(this.big_blind, Math.max(...this.bets.values()) - this.big_blind);
+ } else {
+ return Math.max(this.big_blind, ...this.bets.values());
+ }
+ }
+
+ take_action(user_action) {
+ switch (user_action.action.action) {
+ case "Join":
+ this.seats.set(user_action.username, user_action.action.seat);
+ this.stacks.set(user_action.username, user_action.action.chips);
+ this.hands.set(user_action.username, []);
+ this.num_seats = Math.max(...this.seats.values()) + 1;
+ this.my_seat = this.seats.get(this.username) || 0;
+ this.redraw_players();
+ break;
+ case "Leave":
+ this.seats.delete(user_action.username);
+ this.stacks.delete(user_action.username);
+ this.hands.delete(user_action.username);
+ this.bets.delete(user_action.username);
+ this.num_seats = Math.max(...this.seats.values()) + 1;
+ this.my_seat = this.seats.get(this.username) || 0;
+ this.redraw_players();
+ break;
+ case "KnockedOut":
+ this.seats.delete(user_action.username);
+ this.stacks.delete(user_action.username);
+ if (this.hands.has(user_action.username)) {
+ for (const card of this.hands.get(user_action.username)) {
+ this.svg.removeChild(card.image);
+ }
+ }
+ this.hands.delete(user_action.username);
+ this.bets.delete(user_action.username);
+ if (this.active === user_action.username) {
+ this.active = null;
+ }
+ this.redraw_cards();
+ this.redraw_players();
+ break;
+ case "NextToDeal":
+ this.dealer = user_action.username;
+ for (const card of this.community) {
+ this.svg.removeChild(card.image);
+ }
+ this.community = [];
+ for (const [username, hand] of this.hands) {
+ for (const card of hand) {
+ this.svg.removeChild(card.image);
+ }
+ }
+ this.hands.clear();
+ this.active = null;
+ this.redraw_players();
+ break;
+ case "Bet":
+ case "PostBlind":
+ const current_bet = this.bets.get(user_action.username) || 0;
+ const current_stack = this.stacks.get(user_action.username);
+ this.bets.set(user_action.username, current_bet + user_action.action.chips);
+ this.stacks.set(user_action.username, current_stack - user_action.action.chips);
+ this.active = this.player_after(user_action.username, username => this.hands.has(username) && this.stacks.get(username) > 0);
+ this.redraw_players();
+ break;
+ case "Fold":
+ for (const card of this.hands.get(user_action.username)) {
+ this.svg.removeChild(card.image);
+ }
+ this.hands.delete(user_action.username);
+ this.pot += this.bets.get(user_action.username) || 0;
+ this.active = this.player_after(user_action.username, username => this.hands.has(username) && this.stacks.get(username) > 0);
+ this.redraw_players();
+ break;
+ case "ReceiveCard":
+ case "RevealCard":
+ const card = {
+ card: user_action.action.card,
+ image: this.card_image(user_action.username, user_action.action.card),
+ };
+ if (!this.hands.has(user_action.username)) {
+ this.hands.set(user_action.username, []);
+ }
+ this.hands.get(user_action.username).push(card);
+ this.redraw_cards();
+ break;
+ case "CommunityCard":
+ const community_card = {
+ card: user_action.action.card,
+ image: this.card_image(null, user_action.action.card),
+ };
+ this.community.push(community_card);
+ for (const [username, bet] of this.bets) {
+ this.pot += bet;
+ }
+ this.bets.clear();
+ this.active = this.player_after(user_action.username, username => this.hands.has(username) && this.stacks.get(username) > 0);
+ this.redraw_cards();
+ this.redraw_players();
+ break;
+ case "WinHand":
+ for (const [username, bet] of this.bets) {
+ this.pot += bet;
+ }
+ this.bets.clear();
+ this.pot -= user_action.action.chips;
+ this.stacks.set(user_action.username, this.stacks.get(user_action.username) + user_action.action.chips);
+ this.redraw_players();
+ case "EndDeal":
+ this.active = this.player_after(user_action.username, username => true);
+ this.redraw_players();
+ break;
+ case "WinGame":
+ console.log(this);
+ break;
+ default:
+ console.error("Unhandled action for knock-out whist", user_action);
+ break;
+ }
+ }
+}
import { GameList } from "./gamelist.js";
import { Chatroom } from "./chatroom.js";
import { KnockOutWhist } from "./whist.js";
+import { TexasHoldEm } from "./poker.js";
export class Socket {
constructor(container, login_all_sockets) {
create_game_display(summary, actions) {
this.container.textContent = "";
+ let Format;
switch (this.game.summary.settings.format) {
- case "Chatroom":
- this.game = new Chatroom(this.container, summary, actions, this.auth.username, message => this.send(message));
- break;
- case "KnockOutWhist":
- this.game = new KnockOutWhist(this.container, summary, actions, this.auth.username, message => this.send(message));
- break;
+ case "Chatroom": Format = Chatroom; break;
+ case "KnockOutWhist": Format = KnockOutWhist; break;
+ case "TexasHoldEm": Format = TexasHoldEm; break;
default:
console.error("Unknown format: ", this.game.summary.settings.format);
- break;
+ return;
}
+ this.game = new Format(this.container, summary, actions, this.auth.username, message => this.send(message));
}
onclose() {
@import url("style/login.css");
@import url("style/game-list.css");
@import url("style/chatroom.css");
+@import url("style/poker.css");
@import url("style/whist.css");
html, body {
--- /dev/null
+.controls {
+ fill: grey;
+ stroke: black;
+ stroke-width: 2px;
+}
+
+.fold-control, .call-control, .bet-control {
+ fill: #808080;
+ stroke: black;
+ stroke-width: 1px;
+ transition: fill 0.5s;
+ pointer-events: none;
+}
+
+.fold-control.active, .call-control.active, .bet-control.active {
+ fill: #8080ff;
+ pointer-events: auto;
+}
+
+.fold-control.active:hover, .call-control.active:hover, .bet-control.active:hover {
+ fill: #a0a0ff;
+}
+
+.fold-control-label, .call-control-label, .bet-control-label {
+ pointer-events: none;
+}
}
}
(&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)) => {
+ match (self.server.game_summary(id).await, self.server.game_state(id, 0).await, self.server.game_seed(id).await) {
+ (Ok(summary), Ok(actions), Ok(seed)) => {
let actions_view = actions.iter().map(|action| action.view_for(username)).collect();
- let mut game = Game::new(summary.clone());
+ let mut game = Game::new(summary.clone(), seed);
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{summary, actions: actions_view}
}
- (Err(err), _) => ServerMessage::JoinGameFailure{reason: err.to_string()},
- (_, Err(err)) => ServerMessage::JoinGameFailure{reason: err.to_string()},
+ (Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => ServerMessage::JoinGameFailure{reason: err.to_string()},
}
}
(&mut ClientState::LoggedIn{username, state: LoggedInState::InGame{ref mut game}}, ClientMessage::TakeAction{action}) => {
interests.insert(ClientInterest::Game{id});
server.register_interests(interests).await;
let summary = server.game_summary(id).await?;
- let mut game = Game::new(summary);
+ let seed = server.game_seed(id).await?;
+ let mut game = Game::new(summary, seed);
let mut dealer = Dealer{server, dealer: DealerState{game}};
dealer.retrieve_updates().await?;
Ok(dealer)
use crate::card::{Card, Suit};
use crate::username::Username;
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserAction {
pub username: Username,
pub action: Action,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ValidatedUserAction(pub UserAction);
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "action")]
pub enum Action {
Join { seat: u32, chips: u64 },
ChooseTrumps { suit: Suit },
Fold,
Bet { chips: u64 },
+ PostBlind { chips: u64 },
WinTrick,
WinCall,
- WinHand { chips: u64 },
+ WinHand { chips: u64, hand: Option<String> },
WinGame,
Message { message: String },
Leave,
use std::collections::HashSet;
use std::fmt::{Debug, Display, Formatter};
+use crate::rng::Seed;
use crate::username::Username;
use self::chatroom::{Chatroom, ChatroomSettings};
}
impl dyn Game {
- pub fn new(GameSummary{id, settings}: GameSummary) -> Box<Self> {
+ pub fn new(GameSummary{id, settings}: GameSummary, seed: Seed) -> Box<Self> {
match settings {
GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)),
GameSettings::KnockOutWhist(settings) => Box::new(KnockOutWhist::new(id, settings)),
- GameSettings::TexasHoldEm(settings) => Box::new(TexasHoldEm::new(id, settings)),
+ GameSettings::TexasHoldEm(settings) => Box::new(TexasHoldEm::new(id, settings, seed)),
}
}
}
use itertools::Itertools;
use rand::seq::IteratorRandom;
-use rand::thread_rng;
+use rand::Rng;
use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK};
use crate::seats::Seats;
use crate::username::Username;
use crate::util::max::IteratorMaxItems;
+use crate::rng::{Seed, WaveRng};
use super::super::{Action, ActionError, Game, UserAction, ValidatedUserAction};
pub struct TexasHoldEm {
id: i64,
settings: TexasHoldEmSettings,
+ rng: WaveRng,
actions_len: usize,
state: State,
seats: Seats,
receiver: Option<Username>,
active: Option<Username>,
bets: HashMap<Username, u64>,
+ committed: HashMap<Username, u64>,
players: HashSet<Username>,
pot: u64,
small_blind: u64,
}
impl TexasHoldEm {
- pub fn new(id: i64, settings: TexasHoldEmSettings) -> Self {
+ pub fn new(id: i64, settings: TexasHoldEmSettings, seed: Seed) -> Self {
let small_blind = settings.small_blind;
let big_blind = small_blind * 2;
Self {
id,
settings,
+ rng: seed.rng(),
actions_len: 0,
state: State::NotStarted,
seats: Seats::new(),
receiver: None,
active: None,
bets: HashMap::new(),
+ committed: HashMap::new(),
players: HashSet::new(),
pot: 0,
small_blind,
}
fn chips_to_call(&self, username: Username) -> u64 {
- todo!()
+ match (self.bets.values().max(), self.bets.get(&username)) {
+ (Some(&max), Some(&bet)) => max - bet,
+ (Some(&max), None) => max,
+ (None, Some(_)) | (None, None) => 0,
+ }
+ }
+
+ fn min_raise(&self) -> u64 {
+ match (self.state, self.bets.values().max()) {
+ (State::PreFlopBetting, Some(&max)) if max < self.big_blind * 2 => self.big_blind,
+ (State::PreFlopBetting, Some(&max)) => max - self.big_blind,
+ (_, Some(&max)) => max,
+ (_, None) => self.big_blind,
+ }
}
fn stack(&self, username: Username) -> u64 {
None => 0,
}
}
+
+ fn max_winnings(&self, username: Username) -> u64 {
+ match self.committed.get(&username) {
+ Some(&committed) => self.committed.values().map(|&value| value.min(committed)).sum(),
+ None => 0,
+ }
+ }
+
+ fn is_able_to_bet(&self, username: Username) -> bool {
+ self.players.contains(&username) && !matches!(self.stacks.get(&username), Some(&0) | None)
+ }
+
+ fn players_able_to_bet(&self) -> usize {
+ self.players.iter().map(|&username| self.is_able_to_bet(username)).count()
+ }
}
impl Game for TexasHoldEm {
(_, Action::Fold) | (_, Action::Bet{..}) if self.active != Some(username) => Err(ActionError::OutOfTurn),
(_, Action::Fold) if self.chips_to_call(username) == 0 => Err(ActionError::CannotFold),
(_, Action::Fold) => Ok(ValidatedUserAction(UserAction{username, action: Action::Fold})),
- (_, Action::Bet{chips}) if chips > self.stack(username) => Err(ActionError::NotEnoughChips),
- (_, Action::Bet{chips}) if chips < self.chips_to_call(username) => Err(ActionError::BetSizeTooSmall),
- (_, Action::Bet{chips}) if chips > self.chips_to_call(username) && chips < 2 * self.chips_to_call(username) => Err(ActionError::BetSizeTooSmall),
- (_, Action::Bet{chips}) => Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})),
+ (_, Action::Bet{chips}) => {
+ let stack = self.stack(username);
+ if chips > stack {
+ Err(ActionError::NotEnoughChips)
+ } else if chips == stack {
+ Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}}))
+ } else {
+ let to_call = self.chips_to_call(username);
+ let min_raise = self.min_raise();
+ if chips < to_call {
+ Err(ActionError::BetSizeTooSmall)
+ } else if chips > to_call && chips < to_call + min_raise {
+ Err(ActionError::BetSizeTooSmall)
+ } else {
+ Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}}))
+ }
+ }
+ }
(_, _) => Err(ActionError::InvalidActionForGameType),
}
}
fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> {
self.actions_len += 1;
+ self.rng.advance();
match (self.state, action) {
(_, Action::PlayCard{..}) | (_, Action::ChooseTrumps{..}) => {
Err(ActionError::InvalidActionForGameType)
self.active = None;
self.players = self.seats.player_set();
self.bets.clear();
+ self.committed.clear();
if self.pot != 0 {
error!("Logic error: pot was {} upon dealing: {:#?}", self.pot, self);
self.pot = 0;
}
+ self.state = State::Dealing;
Ok(())
}
(State::Dealing, Action::ReceiveCard{card: Some(card)}) => {
self.receiver = None;
Ok(())
}
- (State::PostingSmallBlind, Action::Bet{chips}) => {
+ (State::PostingSmallBlind, Action::PostBlind{chips}) => {
*self.bets.entry(username).or_default() += chips;
+ *self.committed.entry(username).or_default() += chips;
*self.stacks.entry(username).or_default() -= chips;
self.state = State::PostingBigBlind;
Ok(())
}
- (State::PostingBigBlind, Action::Bet{chips}) => {
+ (State::PostingBigBlind, Action::PostBlind{chips}) => {
*self.bets.entry(username).or_default() += chips;
+ *self.committed.entry(username).or_default() += chips;
*self.stacks.entry(username).or_default() -= chips;
- self.active = self.seats.player_after_in(username, &self.players);
+ self.active = self.seats.player_after_where(username, |username| self.players.contains(&username));
self.state = State::PreFlopBetting;
Ok(())
}
(_, Action::Bet{chips}) => {
- if chips == 0 && self.bets.len() == self.players.len() && self.bets.values().all_equal() {
+ *self.bets.entry(username).or_default() += chips;
+ *self.committed.entry(username).or_default() += chips;
+ *self.stacks.entry(username).or_default() -= chips;
+ if (chips == 0 || !matches!(self.state, State::PreFlopBetting) || self.bets.values().max() > Some(&self.big_blind)) && self.bets.len() == self.players_able_to_bet() && self.bets.values().all_equal()
+ {
self.active = None;
self.pot += self.bets.values().sum::<u64>();
self.bets.clear();
}
};
} else {
- *self.bets.entry(username).or_default() += chips;
- *self.stacks.entry(username).or_default() -= chips;
- self.active = self.seats.player_after_in(username, &self.players);
+ self.active = self.seats.player_after_where(username, |username| self.is_able_to_bet(username));
}
Ok(())
}
self.bets.remove(&username);
self.players.remove(&username);
self.hands.remove(&username);
- self.active = self.seats.player_after_in(username, &self.players);
+ if self.players_able_to_bet() == 1 {
+ self.pot += self.bets.values().sum::<u64>();
+ self.bets.clear();
+ self.active = None;
+ } else {
+ self.active = self.seats.player_after_where(username, |username| self.is_able_to_bet(username));
+ }
Ok(())
}
(State::DealingFlop, Action::CommunityCard{card}) => {
self.community.insert(card);
+ self.deck.remove(&card);
if self.community.len() == 3 {
- self.active = self.seats.player_after_in(username, &self.players);
- self.state = State::PostFlopBetting;
+ self.active = self.seats.player_after_where(username, |username| self.is_able_to_bet(username));
+ self.state = match self.active {
+ Some(_) => State::PostFlopBetting,
+ None => State::DealingTurn,
+ };
}
Ok(())
}
(State::DealingTurn, Action::CommunityCard{card}) => {
self.community.insert(card);
- self.active = self.seats.player_after_in(username, &self.players);
- self.state = State::TurnBetting;
+ self.deck.remove(&card);
+ self.active = self.seats.player_after_where(username, |username| self.is_able_to_bet(username));
+ self.state = match self.active {
+ Some(_) => State::TurnBetting,
+ None => State::DealingRiver,
+ };
Ok(())
}
(State::DealingRiver, Action::CommunityCard{card}) => {
- self.active = self.seats.player_after_in(username, &self.players);
- self.state = State::RiverBetting;
+ self.community.insert(card);
+ self.deck.remove(&card);
+ self.active = self.seats.player_after_where(username, |username| self.is_able_to_bet(username));
+ self.state = match self.active {
+ Some(_) => State::RiverBetting,
+ None => State::Showdown,
+ };
Ok(())
}
- (_, Action::WinHand{chips}) if chips <= self.pot => {
+ (_, Action::WinHand{chips, ..}) if chips <= self.pot => {
self.pot -= chips;
*self.stacks.entry(username).or_default() += chips;
+ self.hands.remove(&username);
+ self.players.remove(&username);
Ok(())
}
(_, Action::WinGame) => {
}
fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
- let mut rng = thread_rng();
+ let mut rng = self.rng.clone();
match self.state {
State::NotStarted => {
if self.seats.players_len() == self.settings.max_players as usize { // TODO
- if let Some(username) = self.seats.player_set().into_iter().choose(&mut rng) {
+ if let Some(username) = rng.choose_from(self.seats.player_set()) {
return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal}));
}
}
}
State::Dealing => {
if let Some(username) = self.receiver {
- let card = self.deck.iter().choose(&mut rng).cloned();
+ let card = rng.choose_from(&self.deck).cloned();
Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}}))
+ } else if let Some(username) = self.dealer {
+ Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal}))
} else {
None
}
}
+ State::PostingSmallBlind if self.seats.players_len() == 2 => {
+ self.dealer
+ .map(|username| {
+ let chips = self.stack(username).min(self.small_blind);
+ ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}})
+ })
+ }
State::PostingSmallBlind => {
self.dealer.and_then(|dealer| self.seats.player_after(dealer))
.map(|username| {
let chips = self.stack(username).min(self.small_blind);
- ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})
+ ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}})
+ })
+ }
+ State::PostingBigBlind if self.seats.players_len() == 2 => {
+ self.dealer.and_then(|dealer| self.seats.player_after(dealer))
+ .map(|username| {
+ let chips = self.stack(username).min(self.small_blind * 2);
+ ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}})
})
}
State::PostingBigBlind => {
self.dealer.and_then(|dealer| self.seats.player_after(dealer))
.and_then(|small_blind| self.seats.player_after(small_blind))
.map(|username| {
- let chips = self.stack(username).min(self.small_blind);
- ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})
+ let chips = self.stack(username).min(self.small_blind * 2);
+ ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}})
})
}
State::PreFlopBetting | State::PostFlopBetting | State::TurnBetting | State::RiverBetting => {
- if self.players.len() == 1 {
+ if self.players.len() <= 1 {
if self.pot > 0 {
self.players.iter().next()
- .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinHand{chips: self.pot}}))
+ .map(|&username| ValidatedUserAction(UserAction {
+ username,
+ action: Action::WinHand {
+ chips: self.pot,
+ hand: None,
+ }
+ }))
} else if self.seats.players_len() == 1 {
- self.players.iter().next()
+ self.seats.player_set().iter().next()
.map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinGame}))
} else if let Some((&username, _)) = self.stacks.iter().find(|&(_, &stack)| stack == 0) {
Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut}))
}
State::DealingFlop | State::DealingTurn | State::DealingRiver => {
self.dealer.and_then(|username|
- self.deck.iter().choose(&mut rng).map(|&card|
+ rng.choose_from(&self.deck).map(|&card|
ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}})))
}
State::Showdown if self.pot == 0 => {
if let Some((&username, _)) = self.stacks.iter().find(|&(_, &stack)| stack == 0) {
Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut}))
+ } else if self.seats.players_len() == 1 {
+ self.seats.player_set().iter().next()
+ .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinGame}))
} else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal}))
} else {
.map(|(&username, hand)| (username, hand.iter().chain(self.community.iter()).cloned().collect::<Vec<_>>()))
.filter_map(|(username, cards)| cards.try_into().ok().map(rank_7_card_hand).map(|hand| (username, hand)))
.max_items_by_key(|(_, hand)| *hand);
+ info!("Showdown: community: {:?}", self.community);
+ info!("Showdown: all hands: {:?}", self.hands);
+ info!("Showdown: winning hands: {:?}", winning_hands);
winning_hands.first()
- .map(|&(username, _)| ValidatedUserAction(UserAction{username, action: Action::WinHand{chips: self.pot / winning_hands.len() as u64}}))
+ .map(|&(username, hand)| ValidatedUserAction(UserAction {
+ username,
+ action: Action::WinHand {
+ chips: (self.pot / winning_hands.len() as u64).min(self.max_winnings(username)),
+ hand: Some(hand.to_string()),
+ }
+ }))
}
State::Completed => None,
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn simple_heads_up_with_2_hands() {
+ let json = r#"[
+ {"username":"p1","action":{"action":"Join","seat":0,"chips":1000}},
+ {"username":"p2","action":{"action":"Join","seat":1,"chips":1000}},
+ {"username":"p1","action":{"action":"NextToDeal"}},
+ {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}},
+ {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}},
+ {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}},
+ {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Diamonds"}}},
+ {"username":"p1","action":{"action":"EndDeal"}},
+ {"username":"p1","action":{"action":"PostBlind","chips":25}},
+ {"username":"p2","action":{"action":"PostBlind","chips":50}},
+ {"username":"p1","action":{"action":"Fold"}},
+ {"username":"p2","action":{"action":"WinHand","chips":75,"hand":null}},
+ {"username":"p2","action":{"action":"NextToDeal"}},
+ {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}},
+ {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}},
+ {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Hearts"}}},
+ {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}},
+ {"username":"p2","action":{"action":"EndDeal"}},
+ {"username":"p2","action":{"action":"PostBlind","chips":25}},
+ {"username":"p1","action":{"action":"PostBlind","chips":50}},
+ {"username":"p2","action":{"action":"Bet","chips":25}},
+ {"username":"p1","action":{"action":"Bet","chips":925}},
+ {"username":"p2","action":{"action":"Bet","chips":925}},
+ {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Hearts"}}},
+ {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Four","suit":"Diamonds"}}},
+ {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Spades"}}},
+ {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}},
+ {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Hearts"}}},
+ {"username":"p2","action":{"action":"WinHand","chips":1950,"hand":"Two Pair, Js & 8s, A Kicker"}},
+ {"username":"p1","action":{"action":"KnockedOut"}},
+ {"username":"p2","action":{"action":"WinGame"}}
+ ]"#;
+
+ let settings = TexasHoldEmSettings {
+ title: "2-Player TexasHoldEm Test".to_string(),
+ max_players: 2,
+ small_blind: 25,
+ starting_stack: 1000,
+ };
+
+ let seed_json = r#"{"rng":"ChaCha20","seed":"f05dc83bdce966e72a3a81b19ccded2e70387eb68deacf60ed8de1ee78b9ff0e"}"#;
+ let seed = serde_json::from_str(seed_json).unwrap();
+
+ let mut game = TexasHoldEm::new(0, settings, seed);
+ let actions: Vec<UserAction> = serde_json::from_str(json).unwrap();
+ for action in actions {
+ match action.action {
+ Action::Join{..} | Action::Bet{..} | Action::Fold => {
+ let validated = game.validate_action(action.clone()).unwrap();
+ assert_eq!(ValidatedUserAction(action), validated);
+ game.take_action(validated).unwrap();
+ }
+ _ => {
+ let dealer_action = game.next_dealer_action().unwrap();
+ assert_eq!(ValidatedUserAction(action), dealer_action);
+ game.take_action(dealer_action).unwrap();
+ }
+ }
+ }
+ }
+}
if self.winners.len() == 1 {
self.call = self.winners.iter().next().cloned();
} else {
- self.receiver = self.dealer.and_then(|dealer| self.seats.player_after_in(dealer, &self.winners));
+ self.receiver = self.dealer.and_then(|dealer| self.seats.player_after_where(dealer, |username| self.winners.contains(&username)));
self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
self.hands.clear();
self.trump_card = None;
mod dealer;
mod game;
mod hands;
+mod rng;
mod seats;
mod server;
mod username;
--- /dev/null
+use std::cmp::{Ord, Eq};
+
+use getrandom::getrandom;
+use itertools::Itertools;
+use rand::{SeedableRng, CryptoRng, RngCore, Rng, Error, seq::IteratorRandom};
+use rand_chacha::ChaCha20Rng;
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+#[serde(tag = "rng")]
+pub enum Seed {
+ ChaCha20 {
+ #[serde(with = "hex")]
+ seed: [u8; 32]
+ },
+}
+
+impl Seed {
+ pub fn cha_cha_20_from_entropy() -> Self {
+ let mut seed = [0u8; 32];
+ getrandom(&mut seed).expect("getrandom failed when generating random seed");
+ Self::ChaCha20{seed}
+ }
+
+ pub fn rng(self) -> WaveRng {
+ match self {
+ Seed::ChaCha20{seed} => {
+ WaveRng::ChaCha20(ChaCha20Rng::from_seed(seed))
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum WaveRng {
+ ChaCha20(ChaCha20Rng),
+}
+
+impl WaveRng {
+ pub fn advance(&mut self) {
+ let _: [u8; 32] = match self {
+ WaveRng::ChaCha20(rng) => rng.gen(),
+ };
+ }
+
+ pub fn choose_from<T: Eq + Ord, I: IntoIterator<Item=T>>(&mut self, elements: I) -> Option<T> {
+ elements.into_iter().sorted().choose_stable(self)
+ }
+}
+
+impl RngCore for WaveRng {
+ fn next_u32(&mut self) -> u32 {
+ match self {
+ WaveRng::ChaCha20(rng) => rng.next_u32(),
+ }
+ }
+
+ fn next_u64(&mut self) -> u64 {
+ match self {
+ WaveRng::ChaCha20(rng) => rng.next_u64(),
+ }
+ }
+
+ fn fill_bytes(&mut self, dest: &mut [u8]) {
+ match self {
+ WaveRng::ChaCha20(rng) => rng.fill_bytes(dest),
+ }
+ }
+
+ fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
+ match self {
+ WaveRng::ChaCha20(rng) => rng.try_fill_bytes(dest),
+ }
+ }
+}
+
+impl CryptoRng for WaveRng {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use std::collections::HashSet;
+
+ use rand::Rng;
+
+ #[test]
+ fn chacha_true_values() {
+ let seed_json = r#"{"rng":"ChaCha20","seed":"0000000000000000000000000000000000000000000000000000000000000000"}"#;
+ let seed: Seed = serde_json::from_str(seed_json).unwrap();
+ let mut rng = seed.rng();
+
+ let mut results = [0u32; 16];
+ for i in results.iter_mut() {
+ *i = rng.next_u32();
+ }
+ let expected = [
+ 0xade0b876, 0x903df1a0, 0xe56a5d40, 0x28bd8653, 0xb819d2bd, 0x1aed8da0, 0xccef36a8,
+ 0xc70d778b, 0x7c5941da, 0x8d485751, 0x3fe02477, 0x374ad8b8, 0xf4b8436a, 0x1ca11815,
+ 0x69b687c3, 0x8665eeb2,
+ ];
+ assert_eq!(results, expected);
+
+ for i in results.iter_mut() {
+ *i = rng.next_u32();
+ }
+ let expected = [
+ 0xbee7079f, 0x7a385155, 0x7c97ba98, 0x0d082d73, 0xa0290fcb, 0x6965e348, 0x3e53c612,
+ 0xed7aee32, 0x7621b729, 0x434ee69c, 0xb03371d5, 0xd539d874, 0x281fed31, 0x45fb0a51,
+ 0x1f0ae1ac, 0x6f4d794b,
+ ];
+ assert_eq!(results, expected);
+ }
+
+ #[test]
+ fn stable_set_values() {
+ let seed_json = r#"{"rng":"ChaCha20","seed":"97a7316097f988e8f3e3d17cc085c9ebe2c132c2aa5310feae73fe11c93f28a8"}"#;
+ let seed: Seed = serde_json::from_str(seed_json).unwrap();
+ let mut rng = seed.rng();
+
+ let mut set = HashSet::new();
+ for i in 0..100 {
+ set.insert(i);
+ }
+
+ assert_eq!(Some(44), rng.choose_from(set));
+ }
+}
}
}
- pub fn player_after_in(&self, username: Username, set: &HashSet<Username>) -> Option<Username> {
+ pub fn player_after_where(&self, username: Username, condition: impl Fn(Username) -> bool) -> Option<Username> {
for (&seat, &player) in &self.players {
if player == username {
for (_, &name) in self.players.range(seat+1..) {
- if set.contains(&name) {
+ if condition(name) {
return Some(name);
}
}
for (_, &name) in self.players.range(..seat) {
- if set.contains(&name) {
+ if condition(name) {
return Some(name);
}
}
use crate::auth::Auth;
use crate::client::ClientInterest;
use crate::game::{GameList, GameSettings, GameSummary, UserAction, ValidatedUserAction};
+use crate::rng::Seed;
use crate::username::Username;
#[derive(Clone)]
format!("game:{}:settings", id)
}
+fn game_seed_key(id: i64) -> String {
+ format!("game:{}:seed", id)
+}
+
fn game_actions_key(id: i64) -> String {
format!("game:{}:actions", id)
}
pub async fn create_game(&mut self, settings: GameSettings) -> RedisResult<i64> {
let id = self.redis.incr("game:next_id", 1).await?;
- let key = game_settings_key(id);
- let () = self.redis.set(key, AsJson(settings)).await?;
+ let () = self.redis.set(game_settings_key(id), AsJson(settings)).await?;
+ let () = self.redis.set(game_seed_key(id), AsJson(Seed::cha_cha_20_from_entropy())).await?;
let () = self.redis.rpush("game:list", id).await?;
Ok(id)
}
pub async fn game_summary(&mut self, id: i64) -> RedisResult<GameSummary> {
let key = game_settings_key(id);
+ info!("Getting summary from key: {}", key);
self.redis.get(key).await.map(AsJson::get).map(|settings| GameSummary::new(id, settings))
}
+ pub async fn game_seed(&mut self, id: i64) -> RedisResult<Seed> {
+ let key = game_seed_key(id);
+ info!("Getting seed from key: {}", key);
+ self.redis.get(key).await.map(AsJson::get)
+ }
+
pub async fn take_action(&mut self, id: i64, len: usize, action: &ValidatedUserAction) -> RedisResult<ActionStatus> {
let key = game_actions_key(id);
debug!("take_action: EVAL {{TAKE_ACTION_LUA_SCRIPT}} 1 {} {} {:?}", key, len, action);
fn from_redis_value(value: &Value) -> RedisResult<Self> {
match value {
Value::Data(ref bytes) => serde_json::from_slice(bytes).map(AsJson)
- .map_err(|err| RedisError::from((
- ErrorKind::TypeError,
- "Failed to parse as JSON",
- err.to_string()
- ))),
+ .map_err(|err| {
+ error!("Failed to parse JSON from redis: {}", err);
+ RedisError::from((
+ ErrorKind::TypeError,
+ "Failed to parse as JSON",
+ err.to_string()
+ ))
+ }),
_ => Err(RedisError::from((ErrorKind::TypeError, "Value was not Value::Data"))),
}
}