├── client ├── __init__.py ├── requirements.txt ├── utils.py ├── poker.py ├── cryptography.py ├── __main__.py ├── watcher.py ├── ui.py └── lib.py ├── .cargo └── config ├── poker ├── src │ ├── lib.rs │ ├── types.rs │ ├── lobby.rs │ ├── game.rs │ ├── deck.rs │ └── poker.rs └── Cargo.toml ├── scripts ├── deploy.sh ├── build.sh └── redeploy.sh ├── .gitignore ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── README.md └── Cargo.lock /client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] -------------------------------------------------------------------------------- /client/requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/mfornet/relaxedjson.py 2 | -------------------------------------------------------------------------------- /poker/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod deck; 2 | mod game; 3 | pub mod lobby; 4 | mod poker; 5 | mod types; 6 | -------------------------------------------------------------------------------- /poker/src/types.rs: -------------------------------------------------------------------------------- 1 | pub type PlayerId = u64; 2 | pub type CardId = u64; 3 | pub type RoomId = u64; 4 | pub type AccountId = String; 5 | pub type CryptoHash = String; 6 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./scripts/build.sh 3 | echo Deploying deck contract to poker 4 | near deploy --wasmFile res/poker.wasm --accountId poker --keyPath neardev/local/poker.json 5 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rustup target add wasm32-unknown-unknown 3 | cargo build -p poker --target wasm32-unknown-unknown --release 4 | mkdir -p res 5 | cp target/wasm32-unknown-unknown/release/poker.wasm ./res/ 6 | -------------------------------------------------------------------------------- /scripts/redeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo Deleting poker contract 3 | near delete poker node0 4 | echo Creating poker account 5 | near create_account poker --masterAccount node0 --initialBalance 1000 --keyPath ~/.near/localnet/node0/validator_key.json 6 | ./scripts/deploy.sh 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | target 3 | res 4 | 5 | # IDE 6 | .idea 7 | .vscode 8 | 9 | # Key pairs generated by the NEAR shell 10 | neardev 11 | 12 | # Misc 13 | .DS_Store 14 | 15 | # Dependencies 16 | node_modules 17 | 18 | __pycache__/ 19 | *.log 20 | 21 | images/ 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["poker"] 3 | 4 | [profile.release] 5 | codegen-units = 1 6 | # Tell `rustc` to optimize for small code size. 7 | opt-level = "z" 8 | lto = true 9 | debug = false 10 | panic = "abort" 11 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 12 | overflow-checks = true 13 | -------------------------------------------------------------------------------- /poker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poker" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = { git = "https://github.com/nearprotocol/json", rev = "1f5779f3b0bd3d2a4b0b975abc46f3d3fe873331", features = ["no_floats"] } 13 | near-bindgen = { version = "0.6.0" } 14 | borsh = "0.6.1" 15 | wee_alloc = "0.4.5" 16 | 17 | [profile.release] 18 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 19 | overflow-checks = true 20 | -------------------------------------------------------------------------------- /client/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | BASE = os.path.expanduser("~/.poker_near") 5 | 6 | 7 | def dump(name, data): 8 | os.makedirs(BASE, exist_ok=True) 9 | with open(os.path.join(BASE, name + '.json'), 'w') as f: 10 | json.dump(data, f, indent=2) 11 | 12 | 13 | def load(name): 14 | os.makedirs(BASE, exist_ok=True) 15 | target = os.path.join(BASE, name + '.json') 16 | 17 | if not os.path.exists(target): 18 | return None 19 | 20 | with open(target) as f: 21 | return json.load(f) 22 | 23 | 24 | def get(dic, *keys): 25 | for key in keys: 26 | try: 27 | if not key in dic: 28 | return None 29 | except TypeError: 30 | return None 31 | dic = dic[key] 32 | return dic 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 NEAR Inc 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /client/poker.py: -------------------------------------------------------------------------------- 1 | def view_function(function): 2 | def real_function(self): 3 | return self.near.view(function.__name__, dict(room_id=self.room_id)) 4 | return real_function 5 | 6 | 7 | class Poker: 8 | def __init__(self, near, room_id): 9 | self.near = near 10 | self.room_id = int(room_id) 11 | 12 | @view_function 13 | def state(self): 14 | pass 15 | 16 | @view_function 17 | def deck_state(self): 18 | pass 19 | 20 | @view_function 21 | def poker_state(self): 22 | pass 23 | 24 | @view_function 25 | def get_partial_shuffle(self): 26 | pass 27 | 28 | @view_function 29 | def get_turn(self): 30 | pass 31 | 32 | def submit_partial_shuffle(self, partial_shuffle): 33 | self.near.change("submit_shuffled", dict( 34 | room_id=self.room_id, new_cards=partial_shuffle)) 35 | 36 | def submit_reveal_part(self, progress): 37 | self.near.change("submit_reveal_part", dict( 38 | room_id=self.room_id, card=progress)) 39 | 40 | def finish_reveal(self): 41 | self.near.change("finish_reveal", dict(room_id=self.room_id)) 42 | -------------------------------------------------------------------------------- /client/cryptography.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | # Some primer numbers of different magnitudes. 4 | # 1000000007 5 | # 1000000000039 6 | # 1000000000000000003 7 | # 1000000000000000000000007 8 | # 1000000000000000000000000000057 9 | # Big prime number. Increase it for more security. 10 | MOD = 1000000000000000003 11 | 12 | 13 | def extended_gcd(a, b): 14 | s, old_s = 0, 1 15 | t, old_t = 1, 0 16 | r, old_r = b, a 17 | 18 | while r != 0: 19 | q = old_r // r 20 | old_r, r = r, old_r - q * r 21 | old_s, s = s, old_s - q * s 22 | old_t, t = t, old_t - q * t 23 | 24 | return old_r, old_s, old_t 25 | 26 | 27 | def generate_secret_key(): 28 | while True: 29 | sk = random.randint(100, MOD - 1) 30 | if extended_gcd(sk, MOD - 1)[0] == 1: 31 | return sk 32 | 33 | 34 | def inverse(a, mod): 35 | g, x, y = extended_gcd(a, mod) 36 | assert g == 1 37 | return (x % mod + mod) % mod 38 | 39 | 40 | def encrypt_and_shuffle(partial_shuffle, secret_key): 41 | partial_shuffle = [pow(num, secret_key, MOD) for num in partial_shuffle] 42 | random.shuffle(partial_shuffle) 43 | return partial_shuffle 44 | 45 | 46 | def partial_decrypt(progress, secret_key): 47 | pw = inverse(secret_key, MOD - 1) 48 | return pow(progress, pw, MOD) 49 | -------------------------------------------------------------------------------- /client/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | from lib import App, register 5 | from watcher import watch 6 | from poker import Poker 7 | from ui import PokerUI 8 | 9 | CONTRACT = 'poker' 10 | 11 | 12 | class PokerCli(App): 13 | @register(name="list", help="List all rooms.") 14 | def list_all(self): 15 | rooms = [(int(room['id']), room['name'], room['status']) 16 | for room in self.near.view("all_rooms", {})] 17 | 18 | if len(rooms) == 0: 19 | print("No rooms found.") 20 | return 21 | 22 | rooms.sort() 23 | for room_id, name, status in rooms: 24 | print(f"{room_id:>03} {name} {status}") 25 | 26 | @register(help=" | Create a new room.") 27 | def new_room(self, name): 28 | room_id = self.near.change("new_room", dict(name=name)) 29 | print(f"Created room {name} with id {room_id}") 30 | 31 | @register(help=" | Enter a room. Can only enter to play in rooms that are Initiating.") 32 | def enter(self, room_id): 33 | room_id = int(room_id) 34 | result = self.near.change("enter", dict(room_id=room_id)) 35 | self.ui.enter(room_id) 36 | watch(self.near, room_id, self.ui) 37 | self.room_id = room_id 38 | 39 | @register(name="start", help=" | Start the game in a room if it is Initiating or Idle") 40 | def _start(self, room_id=None): 41 | if room_id is None: 42 | room_id = self.room_id 43 | room_id = int(room_id) 44 | result = self.near.change("start", dict(room_id=room_id)) 45 | print(result) 46 | 47 | @register(name="raise", help=" | Increase your current bet TO amount.") 48 | def _raise(self, amount, room_id=None): 49 | if room_id is None: 50 | room_id = self.room_id 51 | room_id = int(room_id) 52 | amount = int(amount) 53 | result = self.near.change("submit_bet_action", dict( 54 | room_id=room_id, bet={"Stake": amount})) 55 | print(result) 56 | 57 | @register(help=" | Fold your cards for this round.") 58 | def fold(self, room_id=None): 59 | if room_id is None: 60 | room_id = self.room_id 61 | room_id = int(room_id) 62 | result = self.near.change("submit_bet_action", dict( 63 | room_id=room_id, bet="Fold")) 64 | print(result) 65 | 66 | @register(short="t", help=" | Show game state.") 67 | def state(self, room_id=None): 68 | if room_id is None: 69 | room_id = self.room_id 70 | room_id = int(room_id) 71 | result = self.near.view("state", dict(room_id=room_id)) 72 | print(result) 73 | 74 | @register(help=" | Show raw deck state.") 75 | def deck_state(self, room_id=None): 76 | if room_id is None: 77 | room_id = self.room_id 78 | room_id = int(room_id) 79 | result = self.near.view("deck_state", dict(room_id=room_id)) 80 | print(result) 81 | 82 | @register(help=" | Show raw poker table state.") 83 | def poker_state(self, room_id=None): 84 | if room_id is None: 85 | room_id = self.room_id 86 | room_id = int(room_id) 87 | result = self.near.view("poker_state", dict(room_id=room_id)) 88 | print(result) 89 | 90 | 91 | if __name__ == '__main__': 92 | parser = argparse.ArgumentParser('Poker game') 93 | parser.add_argument( 94 | 'node_key', help="Path to validator key. Usually in neardev/*/.json.") 95 | parser.add_argument('--contract', help="Contract to use", default=CONTRACT) 96 | parser.add_argument('--nodeUrl', help="NEAR Rpc endpoint") 97 | 98 | args = parser.parse_args() 99 | 100 | app = PokerCli(args.node_key, args.contract, args.nodeUrl, PokerUI()) 101 | app.start() 102 | -------------------------------------------------------------------------------- /client/watcher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import time 4 | import os 5 | import hashlib 6 | 7 | from cryptography import encrypt_and_shuffle, partial_decrypt, generate_secret_key 8 | from poker import Poker 9 | from utils import load, dump, get 10 | 11 | 12 | class PokerRoomWatcher(threading.Thread): 13 | def __init__(self, near, room_id, ui): 14 | self.ui = ui 15 | self.room_id = room_id 16 | self.near = near 17 | self.poker = Poker(near, room_id) 18 | self.player_id = None 19 | self.cards = [] 20 | self.load() 21 | super().__init__() 22 | 23 | def load(self): 24 | # Load cards 25 | self.cards = load(self.filename("cards")) or [] 26 | self.ui.cards = self.cards[:] 27 | 28 | # Load secret key 29 | self.secret_key = load(self.filename( 30 | "secret_key")) or generate_secret_key() 31 | self.secret_key = int(self.secret_key) 32 | dump(self.filename("secret_key"), self.secret_key) 33 | 34 | def find_player_id(self): 35 | players = get(self.poker.deck_state(), 'Ok', 'players') 36 | 37 | if not self.near.account_id in players: 38 | logging.debug( 39 | f"{self.near.account_id} is not in game {self.room_id}. Found: {players}") 40 | return True 41 | else: 42 | self.player_id = players.index( 43 | self.near.account_id) 44 | logging.debug( 45 | f"{self.near.account_id} playing in game {self.room_id}. Found {players}") 46 | return False 47 | 48 | def update_state(self): 49 | self._state = self.poker.state() 50 | self._deck_state = self.poker.deck_state() 51 | self._poker_state = self.poker.poker_state() 52 | self._turn = self.poker.get_turn() 53 | self.ui.update_state(self.room_id, self._state, 54 | self._deck_state, self._poker_state, self._turn) 55 | 56 | def is_deck_action(self): 57 | return get(self._state, 'Ok') == 'DeckAction' 58 | 59 | def check_deck_shuffling(self): 60 | if not self.is_deck_action(): 61 | return 62 | 63 | index = get(self._deck_state, 'Ok', 'status', 'Shuffling') 64 | 65 | if index is None: 66 | return 67 | index = int(index) 68 | 69 | if index != self.player_id: 70 | return 71 | 72 | partial_shuffle = self.poker.get_partial_shuffle()["Ok"] 73 | delta = 2 if self.player_id == 0 else 0 74 | partial_shuffle = [int(value) + delta for value in partial_shuffle] 75 | partial_shuffle = encrypt_and_shuffle(partial_shuffle, self.secret_key) 76 | partial_shuffle = [str(value) for value in partial_shuffle] 77 | self.poker.submit_partial_shuffle(partial_shuffle) 78 | 79 | def filename(self, mode): 80 | node_env = os.environ.get("NODE_ENV", "") 81 | chain_enc = f"{self.near.node_url}-{self.near.contract}-{node_env}" 82 | suffix = hashlib.md5(chain_enc.encode()).hexdigest()[:8] 83 | return f"{self.near.account_id}-{self.room_id}-{mode}-{suffix}" 84 | 85 | def on_receive_card(self, card): 86 | if card in self.cards: 87 | return 88 | 89 | self.cards.append(card) 90 | dump(self.filename("cards"), self.cards) 91 | self.ui.update_card(self.room_id, card) 92 | 93 | def check_revealing(self): 94 | if not self.is_deck_action(): 95 | return 96 | 97 | index = get(self._deck_state, 'Ok', 'status', 'Revealing', 'turn') 98 | 99 | if index is None: 100 | return 101 | 102 | index = int(index) 103 | 104 | if index != self.player_id: 105 | return 106 | 107 | progress = int(get(self._deck_state, 'Ok', 108 | 'status', 'Revealing', 'progress')) 109 | 110 | progress = str(partial_decrypt(progress, self.secret_key)) 111 | 112 | if get(self._deck_state, 'Ok', 'status', 'Revealing', 'receiver') == self.player_id: 113 | self.on_receive_card(int(progress) - 2) 114 | self.poker.finish_reveal() 115 | else: 116 | self.poker.submit_reveal_part(progress) 117 | 118 | def step(self): 119 | if self.player_id is None: 120 | if not self.find_player_id(): 121 | return 122 | 123 | self.update_state() 124 | self.check_deck_shuffling() 125 | self.check_revealing() 126 | 127 | def run(self): 128 | time_to_sleep = 1. 129 | 130 | while True: 131 | self.step() 132 | time.sleep(time_to_sleep) 133 | 134 | 135 | WATCHING = set() 136 | 137 | 138 | def watch(near, room_id, ui): 139 | if room_id in WATCHING: 140 | logging.debug(f"Already watching room: {room_id}") 141 | return 142 | 143 | WATCHING.add(room_id) 144 | PokerRoomWatcher(near, room_id, ui).start() 145 | logging.debug(f"Start watching room: {room_id}") 146 | -------------------------------------------------------------------------------- /poker/src/lobby.rs: -------------------------------------------------------------------------------- 1 | use crate::deck::Deck; 2 | use crate::game::{Game, GameError, GameStatus}; 3 | use crate::poker::BetAction; 4 | use crate::poker::Poker; 5 | use crate::types::PlayerId; 6 | use crate::types::{CryptoHash, RoomId}; 7 | use borsh::{BorshDeserialize, BorshSerialize}; 8 | use near_bindgen::near_bindgen; 9 | use serde::Serialize; 10 | use std::collections::HashMap; 11 | 12 | #[global_allocator] 13 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 14 | 15 | #[derive(BorshDeserialize, BorshSerialize, Serialize)] 16 | pub struct RoomInfo { 17 | name: String, 18 | id: RoomId, 19 | status: GameStatus, 20 | } 21 | 22 | impl From<&Game> for RoomInfo { 23 | fn from(poker: &Game) -> Self { 24 | Self { 25 | name: poker.name.clone(), 26 | id: poker.id, 27 | status: poker.status.clone(), 28 | } 29 | } 30 | } 31 | 32 | #[near_bindgen] 33 | #[derive(BorshDeserialize, BorshSerialize, Default, Serialize)] 34 | pub struct Lobby { 35 | last_room: RoomId, 36 | rooms: HashMap, 37 | } 38 | 39 | #[near_bindgen] 40 | impl Lobby { 41 | pub fn new() -> Self { 42 | Self { 43 | last_room: 0, 44 | rooms: HashMap::new(), 45 | } 46 | } 47 | 48 | pub fn new_room(&mut self, name: String) -> RoomId { 49 | let room_id = self.last_room; 50 | self.last_room += 1; 51 | let poker = Game::new(name, room_id); 52 | self.rooms.insert(room_id, poker); 53 | room_id 54 | } 55 | 56 | pub fn all_rooms(&self) -> Vec { 57 | self.rooms.values().map(Into::into).collect() 58 | } 59 | 60 | pub fn all_active_rooms(&self) -> Vec { 61 | self.rooms 62 | .values() 63 | .filter_map(|val| { 64 | if val.status.is_active() { 65 | Some(val) 66 | } else { 67 | None 68 | } 69 | }) 70 | .map(Into::into) 71 | .collect() 72 | } 73 | 74 | pub fn all_initiating_rooms(&self) -> Vec { 75 | self.rooms 76 | .values() 77 | .filter_map(|val| { 78 | if val.status.is_initiating() { 79 | Some(val) 80 | } else { 81 | None 82 | } 83 | }) 84 | .map(Into::into) 85 | .collect() 86 | } 87 | 88 | fn room_ref(&self, room_id: RoomId) -> Result<&Game, GameError> { 89 | self.rooms.get(&room_id).ok_or(GameError::RoomIdNotFound) 90 | } 91 | 92 | fn room_mut(&mut self, room_id: RoomId) -> Result<&mut Game, GameError> { 93 | self.rooms 94 | .get_mut(&room_id) 95 | .ok_or(GameError::RoomIdNotFound) 96 | } 97 | } 98 | 99 | /// Game interface for Lobby 100 | #[near_bindgen] 101 | impl Lobby { 102 | pub fn enter(&mut self, room_id: RoomId) -> Result<(), GameError> { 103 | self.room_mut(room_id)?.enter() 104 | } 105 | 106 | pub fn start(&mut self, room_id: RoomId) -> Result<(), GameError> { 107 | self.room_mut(room_id)?.start() 108 | } 109 | 110 | pub fn close(&mut self, room_id: RoomId) -> Result<(), GameError> { 111 | self.room_mut(room_id)?.close() 112 | } 113 | 114 | pub fn deck_state(&self, room_id: RoomId) -> Result { 115 | Ok(self.room_ref(room_id)?.deck_state()) 116 | } 117 | 118 | pub fn poker_state(&self, room_id: RoomId) -> Result { 119 | Ok(self.room_ref(room_id)?.poker_state()) 120 | } 121 | 122 | pub fn state(&self, room_id: RoomId) -> Result { 123 | Ok(self.room_ref(room_id)?.state()) 124 | } 125 | 126 | pub fn get_turn(&self, room_id: RoomId) -> Result, GameError> { 127 | Ok(self.room_ref(room_id)?.get_turn()) 128 | } 129 | } 130 | 131 | /// Deck interface for Lobby 132 | #[near_bindgen] 133 | impl Lobby { 134 | pub fn get_partial_shuffle(&self, room_id: RoomId) -> Result, GameError> { 135 | self.room_ref(room_id)? 136 | .get_partial_shuffle() 137 | .map_err(Into::into) 138 | } 139 | 140 | pub fn submit_shuffled( 141 | &mut self, 142 | room_id: RoomId, 143 | new_cards: Vec, 144 | ) -> Result<(), GameError> { 145 | self.room_mut(room_id)? 146 | .submit_shuffled(new_cards) 147 | .map_err(Into::into) 148 | } 149 | 150 | pub fn finish_reveal(&mut self, room_id: RoomId) -> Result<(), GameError> { 151 | self.room_mut(room_id)?.finish_reveal().map_err(Into::into) 152 | } 153 | 154 | pub fn submit_reveal_part( 155 | &mut self, 156 | room_id: RoomId, 157 | card: CryptoHash, 158 | ) -> Result<(), GameError> { 159 | self.room_mut(room_id)? 160 | .submit_reveal_part(card) 161 | .map_err(Into::into) 162 | } 163 | } 164 | 165 | /// Poker interface for Lobby 166 | #[near_bindgen] 167 | impl Lobby { 168 | pub fn submit_bet_action(&mut self, room_id: RoomId, bet: BetAction) -> Result<(), GameError> { 169 | self.room_mut(room_id)?.submit_bet_action(bet) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /client/ui.py: -------------------------------------------------------------------------------- 1 | import io 2 | from lib import logging 3 | from utils import get 4 | 5 | SUITES = '♥♠♦♣' 6 | VALUES = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'] 7 | 8 | 9 | def card(num): 10 | num = int(num) 11 | return VALUES[num % 13] + SUITES[num // 13] 12 | 13 | 14 | def build_table(table_i): 15 | cols = [0] * len(table_i[0]) 16 | for row in table_i: 17 | for ix, value in enumerate(row): 18 | cols[ix] = max(cols[ix], len(str(value))) 19 | 20 | for i in range(len(cols)): 21 | cols[i] += 2 22 | 23 | table = io.StringIO() 24 | 25 | first = True 26 | for row in table_i: 27 | if first: 28 | first = False 29 | else: 30 | first_row = True 31 | for col in cols: 32 | if first_row: 33 | first_row = False 34 | else: 35 | print("+", end="", file=table) 36 | print("-" * col, end="", file=table) 37 | print(file=table) 38 | 39 | first_row = True 40 | for ix, value in enumerate(row): 41 | if first_row: 42 | first_row = False 43 | else: 44 | print("|", end="", file=table) 45 | print(str(value).center(cols[ix]), end="", file=table) 46 | print(file=table) 47 | 48 | return table.getvalue() 49 | 50 | 51 | def parse_id(value): 52 | if isinstance(value, str): 53 | return value 54 | elif isinstance(value, dict): 55 | return list(value.keys())[0] 56 | else: 57 | raise ValueError(f"Type not hadled {type(value)}({value})") 58 | 59 | 60 | class PokerUI: 61 | def __init__(self): 62 | self.room_id = None 63 | self.account_id = None 64 | self.cards = [] 65 | self.state = None 66 | self.deck_state = None 67 | self.poker_state = None 68 | self.turn = None 69 | self._last_status = None 70 | 71 | def set_account_id(self, account_id): 72 | self.account_id = account_id 73 | 74 | def enter(self, room_id): 75 | self.room_id = room_id 76 | 77 | def update_table(self): 78 | table = [ 79 | ["Name", "Cards", "Total", "Staked", "Turn"], 80 | ] 81 | 82 | def update_state(self, room_id, state, deck_state, poker_state, turn): 83 | if room_id != self.room_id: 84 | return 85 | 86 | self.state = state 87 | self.deck_state = deck_state 88 | self.poker_state = poker_state 89 | self.turn = get(turn, 'Ok') 90 | logging.debug(repr(self.state)) 91 | logging.debug(repr(self.deck_state)) 92 | logging.debug(repr(self.poker_state)) 93 | logging.debug(repr(self.turn)) 94 | self.display(False) 95 | 96 | def update_card(self, room_id, card): 97 | if room_id != self.room_id: 98 | return 99 | 100 | self.cards.append(card) 101 | self.display() 102 | 103 | def get_action(self): 104 | g_state = get(self.state, 'Ok') 105 | 106 | if g_state == 'PokerAction': 107 | return parse_id(get(self.poker_state, 'Ok', 'status')) 108 | 109 | elif g_state == 'DeckAction': 110 | return parse_id(get(self.deck_state, 'Ok', 'status')) 111 | 112 | else: 113 | return g_state 114 | 115 | def get_revealed_cards(self): 116 | res = get(self.deck_state, 'Ok', 'revealed') 117 | 118 | if res is None: 119 | return [] 120 | 121 | total_players = len(get(self.deck_state, 'Ok', 'players')) 122 | 123 | cards = [] 124 | for card in res[2 * total_players:]: 125 | if card is not None: 126 | cards.append(card) 127 | return cards 128 | 129 | def display(self, force=True): 130 | status = io.StringIO() 131 | print(file=status) 132 | 133 | if self.state is not None: 134 | players = get(self.deck_state, 'Ok', 'players') 135 | tokens = get(self.poker_state, 'Ok', 'tokens') 136 | staked = get(self.poker_state, 'Ok', 'staked') 137 | folded = get(self.poker_state, 'Ok', 'folded') 138 | 139 | action = self.get_action() 140 | turn = self.turn 141 | 142 | if len(players) > 0: 143 | tables = [ 144 | ["Name", "Cards", "Total", "Staked", "On Game", "Turn"], 145 | ] 146 | 147 | # TODO: Show revealed cards after showdown. 148 | if len(self.cards) > 0: 149 | my_cards = ' '.join(map(card, self.cards)) 150 | else: 151 | my_cards = '' 152 | 153 | turn_by_player = False 154 | 155 | for ix, player in enumerate(players): 156 | if player == self.account_id: 157 | player += "(*)" 158 | cards = my_cards 159 | else: 160 | cards = "" 161 | 162 | row = [player, cards, tokens[ix], 163 | staked[ix], not folded[ix], ""] 164 | 165 | if turn == ix: 166 | turn_by_player = True 167 | row[5] = action 168 | 169 | tables.append(row) 170 | 171 | print(build_table(tables), file=status) 172 | 173 | revealed_cards = self.get_revealed_cards() 174 | if len(revealed_cards) > 0: 175 | print("Table:", ' '.join( 176 | map(card, revealed_cards)), file=status) 177 | print(file=status) 178 | 179 | if not turn_by_player: 180 | print("Status:", action, file=status) 181 | print(file=status) 182 | 183 | print(f"[{self.account_id}]>>> ", end="", file=status) 184 | 185 | cur_status = status.getvalue() 186 | if cur_status != self._last_status or force: 187 | print(cur_status, end="") 188 | self._last_status = cur_status 189 | 190 | 191 | if __name__ == '__main__': 192 | print('♥') 193 | -------------------------------------------------------------------------------- /client/lib.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import relaxedjson 4 | from subprocess import PIPE, Popen, check_output 5 | import logging 6 | 7 | logging.basicConfig(filename='poker.log', filemode='a+', 8 | format='%(asctime)s - %(levelname)s - %(message)s', level=logging.DEBUG) 9 | 10 | ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') 11 | 12 | 13 | def get_account_id(path): 14 | with open(path) as f: 15 | data = relaxedjson.parse(f.read()) 16 | return data['account_id'] 17 | 18 | 19 | def parse(inp): 20 | inp = ansi_escape.sub('', inp) 21 | logging.debug(f"Parsing: {repr(inp)}") 22 | return relaxedjson.parse(inp) 23 | 24 | 25 | class Command: 26 | def __init__(self, short, name, help, callback): 27 | self.short = short 28 | self.name = name 29 | self.help = help 30 | self.callback = callback 31 | 32 | 33 | class Near: 34 | def __init__(self, node_key_path, contract, node_url): 35 | self.node_key_path = node_key_path 36 | self.contract = contract 37 | self.node_url = node_url 38 | self.account_id = get_account_id(node_key_path) 39 | 40 | def _parse(self, output): 41 | lines = output.strip('\n').split('\n') 42 | pos = 0 43 | while pos < len(lines) and not lines[pos].startswith("Loaded"): 44 | pos += 1 45 | 46 | if pos == len(lines): 47 | raise ValueError(f"Error parsing output: {output}") 48 | 49 | output = '\n'.join(lines[pos + 1:]) 50 | return parse(output) 51 | 52 | def add_command_url(self, command): 53 | if self.node_url: 54 | command.extend(["--nodeUrl", self.node_url]) 55 | return command 56 | 57 | def view(self, name, args={}): 58 | command = [ 59 | "near", 60 | "view", 61 | self.contract, 62 | name, 63 | json.dumps(args), 64 | "--keyPath", 65 | self.node_key_path, 66 | "--masterAccount", 67 | self.account_id 68 | ] 69 | command = self.add_command_url(command) 70 | 71 | logging.debug(f"View Command: {command}") 72 | proc = Popen(command, stdout=PIPE, stderr=PIPE) 73 | 74 | ret = proc.wait() 75 | logging.debug(f"Exit code: {ret}") 76 | if ret == 0: 77 | result = proc.stdout.read().decode() 78 | return self._parse(result) 79 | else: 80 | logging.warn(f"Command stdout: {proc.stdout.read().decode()}") 81 | 82 | def change(self, name, args={}): 83 | command = [ 84 | "near", 85 | "call", 86 | self.contract, 87 | name, 88 | json.dumps(args), 89 | "--keyPath", 90 | self.node_key_path, 91 | "--accountId", 92 | self.account_id 93 | ] 94 | command = self.add_command_url(command) 95 | 96 | logging.debug(f"Change Command: {command}") 97 | proc = Popen(command, stdout=PIPE, stderr=PIPE) 98 | 99 | ret = proc.wait() 100 | logging.debug(f"Exit code: {ret}") 101 | if ret == 0: 102 | result = proc.stdout.read().decode() 103 | return self._parse(result) 104 | else: 105 | logging.warn(f"Command stdout: {proc.stdout.read().decode()}") 106 | 107 | 108 | def register(function=None, *, short=None, name=None, help=""): 109 | if function is None: 110 | def dec(function): 111 | function._command = True 112 | function._name = name or function.__name__ 113 | function._short = short or function._name[0] 114 | function._help = help 115 | return function 116 | return dec 117 | else: 118 | function._command = True 119 | function._name = name or function.__name__ 120 | function._short = short or function._name[0] 121 | function._help = help 122 | return function 123 | 124 | 125 | class App: 126 | def __init__(self, node_key_path, contract, node_url, ui): 127 | self.near = Near(node_key_path, contract, node_url) 128 | self.ui = ui 129 | self.ui.set_account_id(self.near.account_id) 130 | self._commands = {} 131 | for func_name in dir(self): 132 | if func_name.startswith('__'): 133 | continue 134 | func = self.__getattribute__(func_name) 135 | if '_command' in dir(func): 136 | self._register(func._short, func._name, func._help, func) 137 | 138 | @property 139 | def account_id(self): 140 | return self.near.account_id 141 | 142 | def get_account_id(self): 143 | print(self.account_id) 144 | 145 | def _register(self, short, name, help, callback): 146 | assert not short in self._commands 147 | assert not name in self._commands 148 | command = Command(short, name, help, callback) 149 | self._commands[name] = command 150 | self._commands[short] = command 151 | 152 | @register(help="Show this help") 153 | def help(self, *args): 154 | print("Available commands:") 155 | for key, command in sorted(self._commands.items()): 156 | if key == command.name: 157 | parts = command.help.split('|') 158 | if len(parts) == 2: 159 | _args = parts[0] 160 | _expl = parts[1].strip() 161 | elif len(parts) == 1: 162 | _args = '' 163 | _expl = parts[0].strip() 164 | else: 165 | _args = '' 166 | _expl = '' 167 | print(f"[{command.short}]{command.name:<12} {_expl}") 168 | print((" " * 16) + f"args: {_args}") 169 | print() 170 | 171 | def feed(self, command): 172 | command = command.strip(' ') 173 | if not command: 174 | return 175 | 176 | comm, *args = command.split() 177 | 178 | if not comm in self._commands: 179 | print(f"Command [{comm}] not found.") 180 | self.help() 181 | else: 182 | callback = self._commands[comm].callback 183 | try: 184 | callback(*args) 185 | except Exception as e: 186 | print(*e.args) 187 | print() 188 | 189 | def start(self): 190 | print( 191 | f"Welcome to poker game {self.account_id}. Print [h]help for options.") 192 | logging.info(f"Start game with: {self.account_id}") 193 | 194 | while True: 195 | self.ui.display() 196 | command = input() 197 | self.feed(command) 198 | -------------------------------------------------------------------------------- /poker/src/game.rs: -------------------------------------------------------------------------------- 1 | use crate::deck::{Deck, DeckError, DeckStatus}; 2 | use crate::poker::{ActionResponse, BetAction, Poker, PokerError, PokerStatus}; 3 | use crate::types::{CryptoHash, PlayerId, RoomId}; 4 | use borsh::{BorshDeserialize, BorshSerialize}; 5 | use serde::Serialize; 6 | 7 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Debug)] 8 | pub enum GameError { 9 | RoomIdNotFound, 10 | OngoingRound, 11 | DeckError(DeckError), 12 | PokerError(PokerError), 13 | } 14 | 15 | impl From for GameError { 16 | fn from(deck_error: DeckError) -> Self { 17 | GameError::DeckError(deck_error) 18 | } 19 | } 20 | 21 | impl From for GameError { 22 | fn from(poker_error: PokerError) -> Self { 23 | GameError::PokerError(poker_error) 24 | } 25 | } 26 | 27 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Eq, PartialEq, Clone)] 28 | pub enum GameStatus { 29 | // Start haven't been called. Players are able to enter the game. 30 | Initiating, 31 | // Round already finished. Call start to start next round. 32 | Idle, 33 | // Need action by some player in deck. 34 | DeckAction, 35 | // Need action by some player in poker. 36 | PokerAction, 37 | // Game have been closed 38 | Closed, 39 | } 40 | 41 | impl GameStatus { 42 | pub fn is_active(&self) -> bool { 43 | *self != GameStatus::Closed 44 | } 45 | 46 | pub fn is_initiating(&self) -> bool { 47 | *self == GameStatus::Initiating 48 | } 49 | } 50 | 51 | // TODO: Use NEAR Tokens 52 | #[derive(BorshDeserialize, BorshSerialize, Serialize)] 53 | pub struct Game { 54 | pub name: String, 55 | pub id: RoomId, 56 | pub status: GameStatus, 57 | deck: Deck, 58 | poker: Poker, 59 | } 60 | 61 | impl Game { 62 | pub fn new(name: String, id: RoomId) -> Self { 63 | Self { 64 | name, 65 | id, 66 | status: GameStatus::Initiating, 67 | deck: Deck::new(52), 68 | poker: Poker::new(), 69 | } 70 | } 71 | 72 | pub fn enter(&mut self) -> Result<(), GameError> { 73 | self.deck.enter().map_err(Into::::into)?; 74 | // TODO: Put min tokens / max tokens caps 75 | self.poker.new_player(1000); 76 | Ok(()) 77 | } 78 | 79 | // TODO: An idle game should be started by all players. 80 | pub fn start(&mut self) -> Result<(), GameError> { 81 | match self.status { 82 | GameStatus::Initiating | GameStatus::Idle => { 83 | self.deck.start().map_err(Into::::into)?; 84 | self.status = GameStatus::DeckAction; 85 | Ok(()) 86 | } 87 | _ => Err(GameError::OngoingRound), 88 | } 89 | } 90 | 91 | pub fn close(&mut self) -> Result<(), GameError> { 92 | match self.status { 93 | GameStatus::Initiating | GameStatus::Idle => { 94 | self.deck.close(); 95 | self.status = GameStatus::Closed; 96 | Ok(()) 97 | } 98 | _ => Err(GameError::OngoingRound), 99 | } 100 | } 101 | 102 | fn check_status(&mut self) { 103 | self.status = match self.poker.status { 104 | PokerStatus::Idle => { 105 | self.deck.close(); 106 | GameStatus::Idle 107 | } 108 | PokerStatus::Dealing { 109 | player_id, card_id, .. 110 | } => { 111 | self.deck.reveal_card(card_id, Some(player_id)).expect( 112 | format!( 113 | "Impossible to reveal card {} for player {}", 114 | card_id, player_id 115 | ) 116 | .as_ref(), 117 | ); 118 | 119 | GameStatus::DeckAction 120 | } 121 | PokerStatus::Betting { .. } => GameStatus::PokerAction, 122 | PokerStatus::Revealing { card_id, .. } | PokerStatus::Showdown { card_id, .. } => { 123 | self.deck.reveal_card(card_id, None).expect( 124 | format!("Impossible to reveal card {} for the table.", card_id).as_ref(), 125 | ); 126 | GameStatus::DeckAction 127 | } 128 | PokerStatus::WaitingRevealedCards => { 129 | self.poker.submit_revealed_cards(self.deck.revealed.clone()); 130 | self.deck.close(); 131 | GameStatus::Idle 132 | } 133 | }; 134 | } 135 | 136 | /// Deck finalized one step. 137 | fn check_next_status(&mut self) { 138 | let deck_status = self.deck.get_status(); 139 | 140 | if deck_status != DeckStatus::Running { 141 | self.status = GameStatus::DeckAction; 142 | return; 143 | } 144 | 145 | self.poker.next(); 146 | self.check_status(); 147 | } 148 | 149 | pub fn deck_state(&self) -> Deck { 150 | self.deck.clone() 151 | } 152 | 153 | pub fn poker_state(&self) -> Poker { 154 | self.poker.clone() 155 | } 156 | 157 | pub fn state(&self) -> GameStatus { 158 | self.status.clone() 159 | } 160 | 161 | pub fn player_id(&self) -> Result { 162 | self.deck.get_player_id().map_err(Into::into) 163 | } 164 | 165 | /// Current player that should make an action. 166 | pub fn get_turn(&self) -> Option { 167 | match self.status { 168 | GameStatus::Closed | GameStatus::Idle | GameStatus::Initiating => None, 169 | GameStatus::DeckAction => self.deck.get_turn(), 170 | GameStatus::PokerAction => self.poker.get_turn(), 171 | } 172 | } 173 | 174 | // TODO: Mechanism to slash participants that are stalling the game 175 | // Discussion: Using some number of epochs, elapsed without inactivity. 176 | } 177 | 178 | // Implement Deck public interface for Game 179 | impl Game { 180 | pub fn get_partial_shuffle(&self) -> Result, GameError> { 181 | self.deck.get_partial_shuffle().map_err(Into::into) 182 | } 183 | 184 | pub fn submit_shuffled(&mut self, new_cards: Vec) -> Result<(), GameError> { 185 | self.deck 186 | .submit_shuffled(new_cards) 187 | .map_err(Into::::into)?; 188 | 189 | self.check_next_status(); 190 | Ok(()) 191 | } 192 | 193 | pub fn finish_reveal(&mut self) -> Result<(), GameError> { 194 | self.deck.finish_reveal().map_err(Into::::into)?; 195 | 196 | self.check_next_status(); 197 | Ok(()) 198 | } 199 | 200 | pub fn submit_reveal_part(&mut self, card: CryptoHash) -> Result<(), GameError> { 201 | self.deck 202 | .submit_reveal_part(card) 203 | .map_err(Into::::into)?; 204 | 205 | self.check_next_status(); 206 | Ok(()) 207 | } 208 | } 209 | 210 | // TODO: On Initiating/Idle games allow leaving and claiming back all winned tokens. 211 | // Implement Poker public interface for Game 212 | impl Game { 213 | pub fn submit_bet_action(&mut self, bet: BetAction) -> Result<(), GameError> { 214 | self.poker 215 | .submit_bet_action(ActionResponse { 216 | player_id: self.player_id()?, 217 | action: bet, 218 | }) 219 | .map_err(Into::::into)?; 220 | 221 | self.check_status(); 222 | Ok(()) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /poker/src/deck.rs: -------------------------------------------------------------------------------- 1 | use crate::types::AccountId; 2 | use crate::types::CryptoHash; 3 | use crate::types::{CardId, PlayerId}; 4 | use borsh::{BorshDeserialize, BorshSerialize}; 5 | use near_bindgen::env; 6 | use serde::Serialize; 7 | 8 | #[derive(Serialize, BorshDeserialize, BorshSerialize, Debug)] 9 | pub enum DeckError { 10 | DeckInProgress, 11 | DeckNotInShufflingState, 12 | NotPossibleToStartReveal, 13 | PlayerAlreadyInGame, 14 | PlayerNotInGame, 15 | InvalidTurn, 16 | InvalidPlayerId, 17 | InvalidCardId, 18 | /// Tried to fetch revealed card, but it is not revealed yet. 19 | CardNotRevealed, 20 | /// Tried to reveal part but not in revealing state 21 | NotRevealing, 22 | /// Tried to reveal part but it's not player turn to reveal 23 | PlayerCantReveal, 24 | } 25 | 26 | #[derive(PartialEq, Eq, Clone, BorshDeserialize, BorshSerialize, Serialize, Debug)] 27 | pub enum DeckStatus { 28 | Initiating, 29 | Shuffling(PlayerId), 30 | Running, 31 | /// Revealing progress is ongoing 32 | Revealing { 33 | // Card to be revealed 34 | card_id: CardId, 35 | // Player to whom this card will be revealed. 36 | // None if it is going to be revealed to all players. 37 | receiver: Option, 38 | // Player that should submit its part in this turn. 39 | // It can be the receiver if it should fetch its part. 40 | turn: PlayerId, 41 | // Partially decrypted card. 42 | progress: CryptoHash, 43 | }, 44 | Closed, 45 | } 46 | 47 | impl Default for DeckStatus { 48 | fn default() -> Self { 49 | Self::Initiating 50 | } 51 | } 52 | 53 | #[derive(BorshDeserialize, BorshSerialize, Default, Serialize, Clone)] 54 | pub struct Deck { 55 | status: DeckStatus, 56 | players: Vec, 57 | cards: Vec, 58 | pub revealed: Vec>, 59 | } 60 | 61 | impl Deck { 62 | // TODO: Add password. 63 | // TODO: Add minimum/maximum amount of players. 64 | pub fn new(num_cards: u64) -> Self { 65 | Self { 66 | status: DeckStatus::Initiating, 67 | players: vec![], 68 | cards: (0..num_cards).map(|num| num.to_string()).collect(), 69 | revealed: vec![None; num_cards as usize], 70 | } 71 | } 72 | 73 | pub fn get_players(&self) -> Vec { 74 | self.players.clone() 75 | } 76 | 77 | pub fn num_players(&self) -> u64 { 78 | self.players.len() as u64 79 | } 80 | 81 | pub fn get_player_id(&self) -> Result { 82 | let account_id = env::signer_account_id(); 83 | self.players 84 | .iter() 85 | .position(|player_account_id| player_account_id == &account_id) 86 | .map(|pos| pos as PlayerId) 87 | .ok_or(DeckError::PlayerNotInGame) 88 | } 89 | 90 | pub fn enter(&mut self) -> Result<(), DeckError> { 91 | if self.status == DeckStatus::Initiating { 92 | let account_id = env::signer_account_id(); 93 | if self.players.contains(&account_id) { 94 | Err(DeckError::PlayerAlreadyInGame) 95 | } else { 96 | self.players.push(account_id); 97 | Ok(()) 98 | } 99 | } else { 100 | Err(DeckError::DeckInProgress) 101 | } 102 | } 103 | 104 | pub fn start(&mut self) -> Result<(), DeckError> { 105 | match self.status { 106 | DeckStatus::Initiating | DeckStatus::Closed => { 107 | self.status = DeckStatus::Shuffling(0); 108 | let num_cards = self.cards.len(); 109 | self.cards = (0..num_cards).map(|num| num.to_string()).collect(); 110 | self.revealed = vec![None; num_cards]; 111 | Ok(()) 112 | } 113 | _ => Err(DeckError::DeckInProgress), 114 | } 115 | } 116 | 117 | pub fn get_status(&self) -> DeckStatus { 118 | self.status.clone() 119 | } 120 | 121 | pub fn get_turn(&self) -> Option { 122 | match self.status { 123 | DeckStatus::Closed | DeckStatus::Running | DeckStatus::Initiating => None, 124 | DeckStatus::Shuffling(player_id) => Some(player_id), 125 | DeckStatus::Revealing { turn, .. } => Some(turn), 126 | } 127 | } 128 | 129 | pub fn get_revealed_card(&self, card_id: CardId) -> Result { 130 | self.revealed 131 | .get(card_id as usize) 132 | .ok_or(DeckError::InvalidCardId)? 133 | .clone() 134 | .ok_or(DeckError::CardNotRevealed) 135 | } 136 | 137 | pub fn get_partial_shuffle(&self) -> Result, DeckError> { 138 | if let DeckStatus::Shuffling(_) = self.status { 139 | Ok(self.cards.clone()) 140 | } else { 141 | Err(DeckError::DeckNotInShufflingState) 142 | } 143 | } 144 | 145 | pub fn close(&mut self) { 146 | self.status = DeckStatus::Closed; 147 | } 148 | 149 | // TODO: Add zk-proof for correct computation of the cards (given previous set of cards and public key) 150 | pub fn submit_shuffled(&mut self, new_cards: Vec) -> Result<(), DeckError> { 151 | if let DeckStatus::Shuffling(current_player_id) = self.status { 152 | let player_id = self.get_player_id()?; 153 | if player_id != current_player_id { 154 | Err(DeckError::InvalidTurn) 155 | } else { 156 | self.cards = new_cards; 157 | 158 | if current_player_id + 1 < self.num_players() { 159 | self.status = DeckStatus::Shuffling(current_player_id + 1); 160 | } else { 161 | self.status = DeckStatus::Running; 162 | } 163 | 164 | Ok(()) 165 | } 166 | } else { 167 | Err(DeckError::DeckNotInShufflingState) 168 | } 169 | } 170 | 171 | /// Reveal card at position `card_id` to player at position `player_id`. 172 | /// If `player_id` is None, reveal the card to all 173 | pub fn reveal_card( 174 | &mut self, 175 | card_id: u64, 176 | receiver_player_id: Option, 177 | ) -> Result<(), DeckError> { 178 | if self.status == DeckStatus::Running { 179 | if card_id as usize >= self.cards.len() { 180 | return Err(DeckError::InvalidCardId); 181 | } 182 | 183 | if let Some(receiver_player_id) = receiver_player_id { 184 | let num_players = self.num_players(); 185 | 186 | if receiver_player_id >= num_players { 187 | return Err(DeckError::InvalidPlayerId); 188 | } 189 | 190 | let turn = if num_players == 1 { 191 | 0 192 | } else if receiver_player_id == 0 { 193 | 1 194 | } else { 195 | 0 196 | }; 197 | 198 | self.status = DeckStatus::Revealing { 199 | card_id, 200 | receiver: Some(receiver_player_id), 201 | turn, 202 | progress: self.cards[card_id as usize].clone(), 203 | }; 204 | 205 | Ok(()) 206 | } else { 207 | self.status = DeckStatus::Revealing { 208 | card_id, 209 | receiver: None, 210 | turn: 0, 211 | progress: self.cards[card_id as usize].clone(), 212 | }; 213 | Ok(()) 214 | } 215 | } else { 216 | Err(DeckError::NotPossibleToStartReveal) 217 | } 218 | } 219 | 220 | // TODO: Add zk-proof using previous part and public key 221 | pub fn submit_reveal_part(&mut self, card: CryptoHash) -> Result<(), DeckError> { 222 | if let DeckStatus::Revealing { 223 | card_id, 224 | receiver, 225 | turn, 226 | progress: _, 227 | } = self.status.clone() 228 | { 229 | let player_id = self.get_player_id()?; 230 | 231 | if player_id != turn || receiver.map_or(false, |receiver| player_id == receiver) { 232 | return Err(DeckError::PlayerCantReveal); 233 | } 234 | 235 | let mut next_turn = turn + 1; 236 | 237 | if let Some(receiver) = receiver { 238 | if receiver == next_turn { 239 | next_turn += 1; 240 | } 241 | } 242 | 243 | if next_turn == self.num_players() { 244 | if receiver.is_some() { 245 | self.status = DeckStatus::Revealing { 246 | card_id, 247 | receiver, 248 | turn: receiver.unwrap(), 249 | progress: card, 250 | } 251 | } else { 252 | self.revealed[card_id as usize] = Some(card); 253 | self.status = DeckStatus::Running; 254 | } 255 | } else { 256 | self.status = DeckStatus::Revealing { 257 | card_id, 258 | receiver, 259 | turn: next_turn, 260 | progress: card, 261 | }; 262 | } 263 | 264 | Ok(()) 265 | } else { 266 | Err(DeckError::NotRevealing) 267 | } 268 | } 269 | 270 | /// Receiver of the revealing card should call this function after downloading 271 | /// partially encrypted card to finish the revealing process. 272 | pub fn finish_reveal(&mut self) -> Result<(), DeckError> { 273 | if let DeckStatus::Revealing { 274 | card_id: _, 275 | receiver, 276 | turn, 277 | progress: _, 278 | } = self.status.clone() 279 | { 280 | let player_id = self.get_player_id()?; 281 | 282 | if let Some(receiver) = receiver { 283 | if receiver == player_id && turn == player_id { 284 | self.status = DeckStatus::Running; 285 | return Ok(()); 286 | } 287 | } 288 | Err(DeckError::PlayerCantReveal) 289 | } else { 290 | Err(DeckError::NotRevealing) 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poker 2 | 3 | Play online poker without third parties (and without fees). Bet with NEAR. **Profit**. 4 | Based on [Mental Poker](https://en.wikipedia.org/wiki/Mental_poker) algorithm [proposed by Shamir, Rivest, Adleman](https://apps.dtic.mil/dtic/tr/fulltext/u2/a066331.pdf). 5 | 6 | ## Security details 7 | 8 | The `poker` contract is a "fair game" with some caveats. 9 | 10 | **Pros:** 11 | 12 | - Unbiased deck shuffling. 13 | - Provable and secret card drawing. 14 | 15 | **Cons:** 16 | 17 | - If a player leaves the game the game stalls. Tokens of the player that left the game are slashed and remaining tokens are turned back to other participants. 18 | - ZK-Proofs are required to avoid: 19 | - players submitting invalid data while shuffling deck and partially revealing a card. 20 | - players learning secret information from other players. 21 | 22 | ## Setup 23 | 24 | 1. `npm install -g near-shell`. This will create a global command `near`. Check with `near --version`. 25 | 26 | 2. `pip install -r client/requirements.txt` 27 | 28 | 3. Log in with wallet. Make sure you have access to you keys, usually they are stored on `neardev/**/.json` 29 | - Create an account at [the wallet](https://wallet.nearprotocol.com/). 30 | - Log in using `near login` 31 | 32 | 4. Launch the python client: `python3 client /path/to/account_id_key.json` 33 | 34 | Once you start using the python client watch `poker.log` to see all the interactions taking place with the blockchain. 35 | 36 | ## How to play 37 | 38 | This is how a games look using the client. On start type `help` as suggested to see all commands: 39 | 40 | ``` 41 | [bob]>>> help 42 | Available commands: 43 | [d]deck_state Show raw deck state. 44 | args: 45 | 46 | [e]enter Enter a room. Can only enter to play in rooms that are Initiating. 47 | args: 48 | 49 | [f]fold Fold your cards for this round. 50 | args: 51 | 52 | [h]help Show this help 53 | args: 54 | 55 | [l]list List all rooms. 56 | args: 57 | 58 | [n]new_room Create a new room. 59 | args: 60 | 61 | [p]poker_state Show raw poker table state. 62 | args: 63 | 64 | [r]raise Increase your current bet TO amount. 65 | args: 66 | 67 | [s]start Start the game in a room if it is Initiating or Idle 68 | args: 69 | 70 | [t]state Show game state. 71 | args: 72 | ``` 73 | 74 | ### Create a new room 75 | 76 | ``` 77 | [bob]>>> new_room poker_arena 78 | Created room poker_arena with id 1 79 | ``` 80 | 81 | ### List all rooms 82 | 83 | ``` 84 | [bob]>>> list 85 | 000 qwerty Idle 86 | 001 poker_arena Initiating 87 | ``` 88 | 89 | Each row of the output describes a room: `id name current_status` 90 | You can only enter in rooms with state: `Initiating` 91 | 92 | ### Enter a room 93 | 94 | ``` 95 | [bob]>>> enter 1 96 | 97 | 98 | [bob]>>> 99 | Name | Cards | Total | Staked | On Game | Turn 100 | --------+-------+-------+--------+---------+------ 101 | bob(*) | | 1000 | 0 | True | 102 | 103 | Status: Initiating 104 | ``` 105 | 106 | Notice that the id is an integer. So no need to provide leading 0. 107 | Right now fake tokens are used and all player start with 1000 tokens. 108 | 109 | ### Start the game 110 | 111 | Wait for other players join the game. Board is updated automatically as new players join: 112 | 113 | ``` 114 | Name | Cards | Total | Staked | On Game | Turn 115 | --------+-------+-------+--------+---------+------ 116 | bob(*) | | 1000 | 0 | True | 117 | --------+-------+-------+--------+---------+------ 118 | alice | | 1000 | 0 | True | 119 | --------+-------+-------+--------+---------+------ 120 | carol | | 1000 | 0 | True | 121 | 122 | Status: Initiating 123 | ``` 124 | 125 | After entering a room you don't need to provide room argument necessarily for subsequent commands. Last room entered will be used by default. 126 | 127 | Start the game writing `start`. You should see something similar to this: 128 | 129 | ``` 130 | [bob]>>> 131 | Name | Cards | Total | Staked | On Game | Turn 132 | --------+-------+-------+--------+---------+----------- 133 | bob(*) | | 1000 | 0 | True | 134 | --------+-------+-------+--------+---------+----------- 135 | alice | | 1000 | 0 | True | Shuffling 136 | --------+-------+-------+--------+---------+----------- 137 | carol | | 1000 | 0 | True | 138 | 139 | [bob]>>> 140 | Name | Cards | Total | Staked | On Game | Turn 141 | --------+-------+-------+--------+---------+----------- 142 | bob(*) | | 1000 | 0 | True | 143 | --------+-------+-------+--------+---------+----------- 144 | alice | | 1000 | 0 | True | 145 | --------+-------+-------+--------+---------+----------- 146 | carol | | 1000 | 0 | True | Shuffling 147 | 148 | [bob]>>> 149 | Name | Cards | Total | Staked | On Game | Turn 150 | --------+-------+-------+--------+---------+----------- 151 | bob(*) | | 1000 | 0 | True | 152 | --------+-------+-------+--------+---------+----------- 153 | alice | | 1000 | 0 | True | Shuffling 154 | --------+-------+-------+--------+---------+----------- 155 | carol | | 1000 | 0 | True | 156 | 157 | [bob]>>> 158 | Name | Cards | Total | Staked | On Game | Turn 159 | --------+-------+-------+--------+---------+----------- 160 | bob(*) | | 1000 | 6 | True | 161 | --------+-------+-------+--------+---------+----------- 162 | alice | | 1000 | 0 | True | Revealing 163 | --------+-------+-------+--------+---------+----------- 164 | carol | | 1000 | 3 | True | 165 | 166 | [bob]>>> 167 | ``` 168 | 169 | The board is being updated while state is changing. Initially all players need to shuffle and encrypt the deck (this is done automatically but requires some time). After deck is shuffled initial cards are dealt to participants. 170 | 171 | ``` 172 | [bob]>>> 173 | Name | Cards | Total | Staked | On Game | Turn 174 | --------+--------+-------+--------+---------+--------- 175 | bob(*) | 10♠ 9♦ | 1000 | 6 | True | 176 | --------+--------+-------+--------+---------+--------- 177 | alice | | 1000 | 0 | True | Betting 178 | --------+--------+-------+--------+---------+--------- 179 | carol | | 1000 | 3 | True | 180 | 181 | [bob]>>> 182 | ``` 183 | 184 | For example carol will be seeing a different board: 185 | 186 | ``` 187 | [carol]>>> 188 | Name | Cards | Total | Staked | On Game | Turn 189 | ----------+-------+-------+--------+---------+--------- 190 | bob | | 1000 | 6 | True | 191 | ----------+-------+-------+--------+---------+--------- 192 | alice | | 1000 | 0 | True | Betting 193 | ----------+-------+-------+--------+---------+--------- 194 | carol(*) | 8♦ 9♥ | 1000 | 3 | True | 195 | 196 | [carol]>>> 197 | ``` 198 | 199 | And alice: 200 | 201 | ``` 202 | [alice]>>> 203 | Name | Cards | Total | Staked | On Game | Turn 204 | ----------+-------+-------+--------+---------+--------- 205 | bob | | 1000 | 6 | True | 206 | ----------+-------+-------+--------+---------+--------- 207 | alice(*) | 3♠ 4♦ | 1000 | 0 | True | Betting 208 | ----------+-------+-------+--------+---------+--------- 209 | carol | | 1000 | 3 | True | 210 | 211 | [alice]>>> 212 | ``` 213 | 214 | Staked column denotes how much is at stake at this moment by every participant. Initially there is a big blind of 6 token by player at seat 1, and small blind by previous player (at seat n). Player next to the big blind is first to play, in this case alice at seat 2. 215 | 216 | Turn column denotes which player should play and what is the expected type of action from it. 217 | User interaction in the middle of a round is only required on state `Betting`. 218 | 219 | For the purpose of demonstration we will show the point of view of each player. You can notice which player we are referring to from the context of from the name in the prompt. 220 | 221 | ### Betting 222 | 223 | When is the turn to bet for a player it has two options: (`Fold` and `Raise`). 224 | 225 | **Folding** 226 | 227 | Alice will fold as she has very bad hand and nothing at stake. 228 | 229 | ``` 230 | [alice]>>> fold 231 | {'Ok': None} 232 | 233 | [alice]>>> 234 | Name | Cards | Total | Staked | On Game | Turn 235 | ----------+-------+-------+--------+---------+--------- 236 | bob | | 1000 | 6 | True | 237 | ----------+-------+-------+--------+---------+--------- 238 | alice(*) | 3♠ 4♦ | 1000 | 0 | False | 239 | ----------+-------+-------+--------+---------+--------- 240 | carol | | 1000 | 3 | True | Betting 241 | ``` 242 | 243 | After alice fold its Carol turn. The column *On Game* denotes player that haven't fold so far. Since Alice just fold it is not longer playing this hand. 244 | 245 | **Raise** 246 | 247 | When typing `raise amount` it imply you will increase your stake to `amount` (not adding). It is not valid to raise less than max stake or more than total amount of token. There is a particular case when it is allowed to raise less than max stake and it is when is raised to total token (*All-in*). 248 | 249 | Carol will raise the bet to 10 using: 250 | 251 | ``` 252 | [carol]>>> raise 10 253 | {'Ok': None} 254 | 255 | [carol]>>> 256 | Name | Cards | Total | Staked | On Game | Turn 257 | ----------+-------+-------+--------+---------+--------- 258 | bob | | 1000 | 6 | True | Betting 259 | ----------+-------+-------+--------+---------+--------- 260 | alice | | 1000 | 0 | False | 261 | ----------+-------+-------+--------+---------+--------- 262 | carol(*) | 8♦ 9♥ | 1000 | 10 | True | 263 | 264 | [carol]>>> 265 | ``` 266 | 267 | **Calling** 268 | 269 | And Bob will see Carol bet. 270 | 271 | ``` 272 | [bob]>>> raise 10 273 | {'Ok': None} 274 | 275 | ... 276 | 277 | [bob]>>> 278 | Name | Cards | Total | Staked | On Game | Turn 279 | --------+--------+-------+--------+---------+--------- 280 | bob(*) | 10♠ 9♦ | 1000 | 10 | True | 281 | --------+--------+-------+--------+---------+--------- 282 | alice | | 1000 | 0 | False | 283 | --------+--------+-------+--------+---------+--------- 284 | carol | | 1000 | 10 | True | Betting 285 | 286 | Table: 9♣ 4♥ 8♥ 287 | ``` 288 | 289 | A lot of boards will be displayed before this last board, since to reveal each card all participants needs to interact with the blockchain which might take some time. 290 | 291 | Notice in the bottom of the board the three cards revealed on the *Flop*. 292 | 293 | ## Summary 294 | 295 | Up to this point you have the basics about how to interact with this tool. Notice that rounds might take long time since it requires communication with the blockchain sequentially (not in parallel) from all players. 296 | 297 | ## Deploying the poker contract 298 | 299 | 1. Login with the account you will use to fund the contract. (Let's call it `macboy`). 300 | 301 | 2. Create the account to deploy the contract (Let's call it `poker`) 302 | 303 | `near create_account poker --masterAccount macboy --initialBalance 100 --keyPath neardev/default/macboy.json` 304 | 305 | 3. Build the contract. Requires to have [rust installed](https://www.rust-lang.org/tools/install). 306 | 307 | `./scripts/build.sh` 308 | 309 | 4. Deploy the binary: 310 | 311 | `near deploy --wasmFile res/poker.wasm --accountId poker --keyPath neardev/default/poker.json` 312 | 313 | ## Disclaimer 314 | 315 | This project is work in progress, it is missing some features and has some bugs. See TODO in the code for more details. 316 | 317 | ### Roadmap 318 | 319 | 1. Determine round winners and give pot back to them. 320 | 2. Use NEAR tokens. 321 | 3. Player who loose al its cash should be removed from the game. 322 | 4. Slash participant that stalls the game and recover from that state. 323 | 5. Add ZK-Proof to avoid invalid data while interacting with the deck. 324 | - [Shuffle and encrypt](http://www.csc.kth.se/~terelius/TeWi10Full.pdf) 325 | 6. Improve communication performance. 326 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "Inflector" 5 | version = "0.11.4" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 8 | dependencies = [ 9 | "lazy_static", 10 | "regex", 11 | ] 12 | 13 | [[package]] 14 | name = "aho-corasick" 15 | version = "0.7.10" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" 18 | dependencies = [ 19 | "memchr", 20 | ] 21 | 22 | [[package]] 23 | name = "autocfg" 24 | version = "1.0.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 27 | 28 | [[package]] 29 | name = "base64" 30 | version = "0.11.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 33 | 34 | [[package]] 35 | name = "block-buffer" 36 | version = "0.7.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" 39 | dependencies = [ 40 | "block-padding", 41 | "byte-tools", 42 | "byteorder", 43 | "generic-array", 44 | ] 45 | 46 | [[package]] 47 | name = "block-padding" 48 | version = "0.1.5" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 51 | dependencies = [ 52 | "byte-tools", 53 | ] 54 | 55 | [[package]] 56 | name = "borsh" 57 | version = "0.6.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "2f9dada4c07fa726bc195503048581e7b1719407f7fbef82741f7b149d3921b3" 60 | dependencies = [ 61 | "borsh-derive", 62 | ] 63 | 64 | [[package]] 65 | name = "borsh-derive" 66 | version = "0.6.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "47c6bed3dd7695230e85bd51b6a4e4e4dc7550c1974a79c11e98a8a055211a61" 69 | dependencies = [ 70 | "borsh-derive-internal", 71 | "borsh-schema-derive-internal", 72 | "syn", 73 | ] 74 | 75 | [[package]] 76 | name = "borsh-derive-internal" 77 | version = "0.6.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "d34f80970434cd6524ae676b277d024b87dd93ecdd3f53bf470d61730dc6cb80" 80 | dependencies = [ 81 | "proc-macro2", 82 | "quote", 83 | "syn", 84 | ] 85 | 86 | [[package]] 87 | name = "borsh-schema-derive-internal" 88 | version = "0.6.1" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "b3b93230d3769ea99ac75a8a7fee2a229defbc56fe8816c9cde8ed78c848aa33" 91 | dependencies = [ 92 | "proc-macro2", 93 | "quote", 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "bs58" 99 | version = "0.3.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "b170cd256a3f9fa6b9edae3e44a7dfdfc77e8124dbc3e2612d75f9c3e2396dae" 102 | 103 | [[package]] 104 | name = "byte-tools" 105 | version = "0.3.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 108 | 109 | [[package]] 110 | name = "byteorder" 111 | version = "1.3.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "0.1.10" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 120 | 121 | [[package]] 122 | name = "digest" 123 | version = "0.8.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 126 | dependencies = [ 127 | "generic-array", 128 | ] 129 | 130 | [[package]] 131 | name = "fake-simd" 132 | version = "0.1.2" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 135 | 136 | [[package]] 137 | name = "generic-array" 138 | version = "0.12.3" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" 141 | dependencies = [ 142 | "typenum", 143 | ] 144 | 145 | [[package]] 146 | name = "indexmap" 147 | version = "1.3.2" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" 150 | dependencies = [ 151 | "autocfg", 152 | ] 153 | 154 | [[package]] 155 | name = "itoa" 156 | version = "0.4.5" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 159 | 160 | [[package]] 161 | name = "keccak" 162 | version = "0.1.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" 165 | 166 | [[package]] 167 | name = "lazy_static" 168 | version = "1.4.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 171 | 172 | [[package]] 173 | name = "libc" 174 | version = "0.2.69" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 177 | 178 | [[package]] 179 | name = "memchr" 180 | version = "2.3.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 183 | 184 | [[package]] 185 | name = "memory_units" 186 | version = "0.4.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 189 | 190 | [[package]] 191 | name = "near-bindgen" 192 | version = "0.6.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "e003a36be48d43819734311509fbad34fa209e4c3b90daaa76ece622cb1eea83" 195 | dependencies = [ 196 | "borsh", 197 | "near-bindgen-macros", 198 | "near-runtime-fees", 199 | "near-vm-logic", 200 | "serde", 201 | ] 202 | 203 | [[package]] 204 | name = "near-bindgen-core" 205 | version = "0.6.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "c13050bce4261e228ecfe376e36b633752cae098ce22876e662fdfeee5e04fb4" 208 | dependencies = [ 209 | "Inflector", 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "near-bindgen-macros" 217 | version = "0.6.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "b9ff73f282724c5d6a6860f173dd88d6fe623da3b1030549c41a6eb808fd45ed" 220 | dependencies = [ 221 | "near-bindgen-core", 222 | "proc-macro2", 223 | "quote", 224 | "syn", 225 | ] 226 | 227 | [[package]] 228 | name = "near-rpc-error-core" 229 | version = "0.1.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "ffa8dbf8437a28ac40fcb85859ab0d0b8385013935b000c7a51ae79631dd74d9" 232 | dependencies = [ 233 | "proc-macro2", 234 | "quote", 235 | "serde", 236 | "serde_json 1.0.51", 237 | "syn", 238 | ] 239 | 240 | [[package]] 241 | name = "near-rpc-error-macro" 242 | version = "0.1.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "0c6111d713e90c7c551dee937f4a06cb9ea2672243455a4454cc7566387ba2d9" 245 | dependencies = [ 246 | "near-rpc-error-core", 247 | "proc-macro2", 248 | "quote", 249 | "serde", 250 | "serde_json 1.0.51", 251 | "syn", 252 | ] 253 | 254 | [[package]] 255 | name = "near-runtime-fees" 256 | version = "0.6.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "2dbca6d037e0e9936bd5e60399d4c15adbb186a3af40bfb1d8c15f200c00873d" 259 | dependencies = [ 260 | "serde", 261 | ] 262 | 263 | [[package]] 264 | name = "near-vm-errors" 265 | version = "0.6.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "d9b8531f7025a3b13be8185621ea43c311ac8bd1426d26f3ac246e09b21c8681" 268 | dependencies = [ 269 | "borsh", 270 | "near-rpc-error-macro", 271 | "serde", 272 | ] 273 | 274 | [[package]] 275 | name = "near-vm-logic" 276 | version = "0.6.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "fdfffa381f4609ff47b67e68550a3acb628367ed88a40e3f6b37d0a8248d2b02" 279 | dependencies = [ 280 | "base64", 281 | "bs58", 282 | "byteorder", 283 | "near-runtime-fees", 284 | "near-vm-errors", 285 | "serde", 286 | "sha2", 287 | "sha3", 288 | ] 289 | 290 | [[package]] 291 | name = "opaque-debug" 292 | version = "0.2.3" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 295 | 296 | [[package]] 297 | name = "poker" 298 | version = "0.1.0" 299 | dependencies = [ 300 | "borsh", 301 | "near-bindgen", 302 | "serde", 303 | "serde_json 1.0.40", 304 | "wee_alloc", 305 | ] 306 | 307 | [[package]] 308 | name = "proc-macro2" 309 | version = "1.0.10" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 312 | dependencies = [ 313 | "unicode-xid", 314 | ] 315 | 316 | [[package]] 317 | name = "quote" 318 | version = "1.0.3" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 321 | dependencies = [ 322 | "proc-macro2", 323 | ] 324 | 325 | [[package]] 326 | name = "regex" 327 | version = "1.3.6" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" 330 | dependencies = [ 331 | "aho-corasick", 332 | "memchr", 333 | "regex-syntax", 334 | "thread_local", 335 | ] 336 | 337 | [[package]] 338 | name = "regex-syntax" 339 | version = "0.6.17" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" 342 | 343 | [[package]] 344 | name = "ryu" 345 | version = "1.0.3" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" 348 | 349 | [[package]] 350 | name = "serde" 351 | version = "1.0.106" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 354 | dependencies = [ 355 | "serde_derive", 356 | ] 357 | 358 | [[package]] 359 | name = "serde_derive" 360 | version = "1.0.106" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 363 | dependencies = [ 364 | "proc-macro2", 365 | "quote", 366 | "syn", 367 | ] 368 | 369 | [[package]] 370 | name = "serde_json" 371 | version = "1.0.40" 372 | source = "git+https://github.com/nearprotocol/json?rev=1f5779f3b0bd3d2a4b0b975abc46f3d3fe873331#1f5779f3b0bd3d2a4b0b975abc46f3d3fe873331" 373 | dependencies = [ 374 | "itoa", 375 | "ryu", 376 | "serde", 377 | ] 378 | 379 | [[package]] 380 | name = "serde_json" 381 | version = "1.0.51" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" 384 | dependencies = [ 385 | "indexmap", 386 | "itoa", 387 | "ryu", 388 | "serde", 389 | ] 390 | 391 | [[package]] 392 | name = "sha2" 393 | version = "0.8.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" 396 | dependencies = [ 397 | "block-buffer", 398 | "digest", 399 | "fake-simd", 400 | "opaque-debug", 401 | ] 402 | 403 | [[package]] 404 | name = "sha3" 405 | version = "0.8.2" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" 408 | dependencies = [ 409 | "block-buffer", 410 | "byte-tools", 411 | "digest", 412 | "keccak", 413 | "opaque-debug", 414 | ] 415 | 416 | [[package]] 417 | name = "syn" 418 | version = "1.0.17" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 421 | dependencies = [ 422 | "proc-macro2", 423 | "quote", 424 | "unicode-xid", 425 | ] 426 | 427 | [[package]] 428 | name = "thread_local" 429 | version = "1.0.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 432 | dependencies = [ 433 | "lazy_static", 434 | ] 435 | 436 | [[package]] 437 | name = "typenum" 438 | version = "1.12.0" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 441 | 442 | [[package]] 443 | name = "unicode-xid" 444 | version = "0.2.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 447 | 448 | [[package]] 449 | name = "wee_alloc" 450 | version = "0.4.5" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" 453 | dependencies = [ 454 | "cfg-if", 455 | "libc", 456 | "memory_units", 457 | "winapi", 458 | ] 459 | 460 | [[package]] 461 | name = "winapi" 462 | version = "0.3.8" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 465 | dependencies = [ 466 | "winapi-i686-pc-windows-gnu", 467 | "winapi-x86_64-pc-windows-gnu", 468 | ] 469 | 470 | [[package]] 471 | name = "winapi-i686-pc-windows-gnu" 472 | version = "0.4.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 475 | 476 | [[package]] 477 | name = "winapi-x86_64-pc-windows-gnu" 478 | version = "0.4.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 481 | -------------------------------------------------------------------------------- /poker/src/poker.rs: -------------------------------------------------------------------------------- 1 | use crate::types::CardId; 2 | use crate::types::CryptoHash; 3 | use crate::types::PlayerId; 4 | use borsh::{BorshDeserialize, BorshSerialize}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Clone, PartialEq, Eq)] 8 | pub enum Stage { 9 | Flop, 10 | Turn, 11 | River, 12 | Showdown, 13 | } 14 | 15 | impl Stage { 16 | fn next(&self) -> Self { 17 | match self { 18 | Stage::Flop => Stage::Turn, 19 | Stage::Turn => Stage::River, 20 | Stage::River => Stage::Showdown, 21 | Stage::Showdown => panic!("No next stage after showdown"), 22 | } 23 | } 24 | 25 | fn cards_to_reveal(&self) -> u8 { 26 | match self { 27 | Stage::Flop => 3, 28 | Stage::Turn => 1, 29 | Stage::River => 1, 30 | Stage::Showdown => panic!("No cards to reveal at showdown"), 31 | } 32 | } 33 | } 34 | 35 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Clone, Eq, PartialEq)] 36 | pub enum PokerStatus { 37 | Idle, 38 | Dealing { 39 | player_id: PlayerId, 40 | card_id: CardId, 41 | first_card: bool, 42 | }, 43 | Betting { 44 | // Waiting for player `target` to make an action. 45 | target: PlayerId, 46 | // Last player to raise. If raised is false this is big blind. 47 | until: PlayerId, 48 | // If some player have raised in this round. 49 | raised: bool, 50 | // Current max stake that must be called or raised 51 | max_stake: u64, 52 | // Next stage to play. 53 | next_stage: Stage, 54 | }, 55 | Revealing { 56 | stage: Stage, 57 | card_id: CardId, 58 | missing_to_reveal: u8, 59 | }, 60 | Showdown { 61 | player_id: PlayerId, 62 | card_id: CardId, 63 | first_card: bool, 64 | }, 65 | WaitingRevealedCards, 66 | } 67 | 68 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Debug)] 69 | pub enum PokerError { 70 | InvalidPlayerId, 71 | 72 | TooLowStake, 73 | NotEnoughStake, 74 | 75 | NotBettingRound, 76 | NotBettingTurn, 77 | } 78 | 79 | #[derive(Serialize, Deserialize)] 80 | pub enum BetAction { 81 | Fold, 82 | Stake(u64), 83 | } 84 | 85 | pub struct ActionResponse { 86 | pub player_id: PlayerId, 87 | pub action: BetAction, 88 | } 89 | 90 | // TODO: Remove automatically from the game players with 0 tokens. 91 | // Mainly regarding card signatures. 92 | 93 | /// Raw poker implementation. 94 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Clone)] 95 | pub struct Poker { 96 | /// Number of tokens each player has available. 97 | tokens: Vec, 98 | /// Currently staked tokens. 99 | staked: Vec, 100 | /// Players that have already folded its cards in this turn. 101 | folded: Vec, 102 | /// Current status. 103 | pub status: PokerStatus, 104 | /// Number of token used for the big blind. This value will double on each game. 105 | blind_token: u64, 106 | /// Player which is the big blind on next round. 107 | big_blind: PlayerId, 108 | /// Card on the top of the stack. 109 | first_unrevealed_card: CardId, 110 | } 111 | 112 | impl Poker { 113 | pub fn new() -> Self { 114 | Self { 115 | tokens: vec![], 116 | staked: vec![], 117 | folded: vec![], 118 | status: PokerStatus::Idle, 119 | blind_token: 6, 120 | big_blind: 0, 121 | first_unrevealed_card: 0, 122 | } 123 | } 124 | 125 | pub fn new_player(&mut self, tokens: u64) { 126 | self.tokens.push(tokens); 127 | self.staked.push(0); 128 | self.folded.push(false); 129 | } 130 | 131 | fn prev_player(&self, player_id: PlayerId) -> PlayerId { 132 | if player_id == 0 { 133 | self.num_players() - 1 134 | } else { 135 | player_id - 1 136 | } 137 | } 138 | 139 | fn next_player(&self, player_id: PlayerId) -> PlayerId { 140 | if player_id + 1 == self.num_players() { 141 | 0 142 | } else { 143 | player_id + 1 144 | } 145 | } 146 | 147 | fn next_on_game(&self, mut player_id: PlayerId) -> PlayerId { 148 | for _ in 0..self.num_players() { 149 | if !self.folded[player_id as usize] { 150 | return player_id; 151 | } else { 152 | player_id = self.next_player(player_id); 153 | } 154 | } 155 | panic!("All players folded."); 156 | } 157 | 158 | /// Number of players who have already folded 159 | fn total_folded(&self) -> u64 { 160 | self.folded.iter().filter(|&folded| *folded).count() as u64 161 | } 162 | 163 | /// Increase the stake for player_id to stake. If stake is less than current staked 164 | /// by this player, it will return Error without changing anything. 165 | /// If stake is bigger than total tokens, it will stake all tokens. 166 | fn try_stake(&mut self, player_id: PlayerId, stake: u64) -> Result<(), PokerError> { 167 | let total = self 168 | .tokens 169 | .get(player_id as usize) 170 | .ok_or(PokerError::InvalidPlayerId)?; 171 | 172 | if stake < self.staked[player_id as usize] { 173 | return Err(PokerError::TooLowStake); 174 | } 175 | 176 | self.staked[player_id as usize] = std::cmp::min(stake, *total); 177 | Ok(()) 178 | } 179 | 180 | pub fn get_status(&self) -> PokerStatus { 181 | self.status.clone() 182 | } 183 | 184 | pub fn get_turn(&self) -> Option { 185 | match self.status { 186 | PokerStatus::Betting { target, .. } => Some(target), 187 | _ => None, 188 | } 189 | } 190 | 191 | /// Get topmost card not used yet. Mark this card as used. 192 | fn get_card(&mut self) -> CardId { 193 | self.first_unrevealed_card += 1; 194 | self.first_unrevealed_card - 1 195 | } 196 | 197 | fn card_id_from_player(&self, player_id: PlayerId, first_card: bool) -> CardId { 198 | 2 * player_id + (!first_card as u64) 199 | } 200 | 201 | pub fn next(&mut self) { 202 | match self.status.clone() { 203 | PokerStatus::Idle => { 204 | // Make small blind and big blinds bet 205 | self.try_stake(self.big_blind, self.blind_token).unwrap(); 206 | self.try_stake(self.prev_player(self.big_blind), self.blind_token / 2) 207 | .unwrap(); 208 | 209 | self.status = PokerStatus::Dealing { 210 | player_id: 0, 211 | card_id: self.get_card(), 212 | first_card: true, 213 | }; 214 | } 215 | PokerStatus::Dealing { 216 | player_id, 217 | first_card, 218 | .. 219 | } => { 220 | if first_card { 221 | self.status = PokerStatus::Dealing { 222 | player_id, 223 | card_id: self.get_card(), 224 | first_card: false, 225 | }; 226 | } else { 227 | if player_id + 1 == self.num_players() { 228 | // All cards where already dealt. Start first round of betting. 229 | let target = self.next_player(self.big_blind); 230 | self.status = PokerStatus::Betting { 231 | target, 232 | until: self.big_blind, 233 | raised: false, 234 | max_stake: self.blind_token, 235 | next_stage: Stage::Flop, 236 | }; 237 | } else { 238 | self.status = PokerStatus::Dealing { 239 | player_id: player_id + 1, 240 | card_id: self.get_card(), 241 | first_card: true, 242 | }; 243 | } 244 | } 245 | } 246 | PokerStatus::Revealing { 247 | stage, 248 | missing_to_reveal, 249 | .. 250 | } => { 251 | if missing_to_reveal == 0 { 252 | // Find next player who have not folded after the big blind. 253 | let target = self.next_on_game(self.next_player(self.big_blind)); 254 | self.status = PokerStatus::Betting { 255 | target, 256 | until: self.big_blind, 257 | raised: false, 258 | max_stake: self.blind_token, 259 | next_stage: stage.next(), 260 | }; 261 | } else { 262 | self.status = PokerStatus::Revealing { 263 | stage, 264 | card_id: self.get_card(), 265 | missing_to_reveal: missing_to_reveal - 1, 266 | }; 267 | } 268 | } 269 | // TODO: Fix issue with infinite showdown loop 270 | PokerStatus::Showdown { 271 | player_id, 272 | first_card, 273 | .. 274 | } => { 275 | if first_card { 276 | self.status = PokerStatus::Showdown { 277 | player_id, 278 | card_id: self.card_id_from_player(player_id, false), 279 | first_card: false, 280 | }; 281 | } else { 282 | let next_player = self.next_on_game(player_id); 283 | 284 | if next_player < player_id { 285 | // All cards were revealed 286 | self.status = PokerStatus::WaitingRevealedCards; 287 | } else { 288 | self.status = PokerStatus::Showdown { 289 | player_id: next_player, 290 | card_id: self.card_id_from_player(player_id, true), 291 | first_card: true, 292 | }; 293 | } 294 | } 295 | } 296 | PokerStatus::Betting { .. } => panic!("Called next on betting state."), 297 | PokerStatus::WaitingRevealedCards => { 298 | panic!("Called next while waiting for revealed cards.") 299 | } 300 | } 301 | } 302 | 303 | /// Call when the round is over. Update the state of the game for the next round. 304 | fn finish(&mut self, _winners: Vec) { 305 | self.status = PokerStatus::Idle; 306 | self.big_blind = self.next_player(self.big_blind); 307 | self.blind_token *= 2; 308 | self.first_unrevealed_card = 0; 309 | 310 | // TODO: Reassign stake to winners. 311 | // TODO: Reset state 312 | } 313 | 314 | fn start_stage(&mut self, stage: Stage) { 315 | if stage == Stage::Showdown { 316 | let player_id = self.next_on_game(0); 317 | self.status = PokerStatus::Showdown { 318 | player_id, 319 | card_id: self.card_id_from_player(player_id, true), 320 | first_card: true, 321 | }; 322 | } else { 323 | let missing_to_reveal = stage.cards_to_reveal() - 1; 324 | self.status = PokerStatus::Revealing { 325 | stage, 326 | card_id: self.get_card(), 327 | missing_to_reveal, 328 | }; 329 | } 330 | } 331 | 332 | pub fn submit_revealed_cards(&mut self, _cards: Vec>) { 333 | if self.status != PokerStatus::WaitingRevealedCards { 334 | panic!("Not waiting revealed cards"); 335 | } 336 | 337 | // TODO: Find winners from cards revealed. 338 | 339 | self.finish(vec![]); 340 | 341 | todo!(); 342 | } 343 | 344 | /// Submit the bet option from the player that is its turn. 345 | pub fn submit_bet_action(&mut self, action: ActionResponse) -> Result<(), PokerError> { 346 | match self.status.clone() { 347 | PokerStatus::Betting { 348 | target, 349 | until, 350 | raised, 351 | max_stake, 352 | next_stage, 353 | } => { 354 | if target != action.player_id { 355 | return Err(PokerError::NotBettingTurn); 356 | } 357 | 358 | match action.action { 359 | BetAction::Fold => { 360 | self.folded[action.player_id as usize] = true; 361 | let next_player = self.next_on_game(action.player_id); 362 | 363 | if self.total_folded() + 1 == self.num_players() { 364 | // All players but one have folded. That is the winner. 365 | self.finish(vec![next_player]); 366 | Ok(()) 367 | } else { 368 | self.status = PokerStatus::Betting { 369 | target: next_player, 370 | until, 371 | raised, 372 | max_stake, 373 | next_stage, 374 | }; 375 | Ok(()) 376 | } 377 | } 378 | BetAction::Stake(stake) => { 379 | if stake < max_stake { 380 | Err(PokerError::TooLowStake) 381 | } else if stake > self.tokens[action.player_id as usize] { 382 | Err(PokerError::NotEnoughStake) 383 | } else { 384 | self.staked[action.player_id as usize] = stake; 385 | 386 | if stake > max_stake { 387 | // Raise 388 | // TODO: Put a lower bound on raising 389 | let next_player = 390 | self.next_on_game(self.next_player(action.player_id)); 391 | 392 | self.status = PokerStatus::Betting { 393 | target: next_player, 394 | until: next_player, 395 | raised: true, 396 | max_stake: stake, 397 | next_stage, 398 | }; 399 | Ok(()) 400 | } else { 401 | // Call 402 | if action.player_id == until { 403 | // Finish betting round. All players call big blind bet. 404 | self.start_stage(next_stage); 405 | Ok(()) 406 | } else { 407 | let next_player = 408 | self.next_on_game(self.next_player(action.player_id)); 409 | if next_player != until || !raised { 410 | // Missing some players to place their bets. 411 | self.status = PokerStatus::Betting { 412 | target: next_player, 413 | until, 414 | raised, 415 | max_stake, 416 | next_stage, 417 | }; 418 | Ok(()) 419 | } else { 420 | // Finish betting round. All players call last raised stake. 421 | self.start_stage(next_stage); 422 | Ok(()) 423 | } 424 | } 425 | } 426 | } 427 | } 428 | } 429 | } 430 | _ => Err(PokerError::NotBettingRound), 431 | } 432 | } 433 | 434 | fn num_players(&self) -> u64 { 435 | self.tokens.len() as u64 436 | } 437 | } 438 | --------------------------------------------------------------------------------