├── agi ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── nl_holdem_env.cpython-37.pyc │ ├── nl_holdem_net.cpython-37.pyc │ └── evaluation_tools.cpython-37.pyc ├── evaluation_tools.py ├── resnet.py ├── league.py ├── nl_holdem_net.py ├── nl_holdem_lg_net.py └── nl_holdem_env.py ├── imgs ├── logo.png ├── trainig.jpg └── play_against_ai.jpg ├── weights └── c_1048.pkl ├── gui ├── static │ ├── cards │ │ ├── C2.png │ │ ├── C3.png │ │ ├── C4.png │ │ ├── C5.png │ │ ├── C6.png │ │ ├── C7.png │ │ ├── C8.png │ │ ├── C9.png │ │ ├── CA.png │ │ ├── CJ.png │ │ ├── CK.png │ │ ├── CQ.png │ │ ├── CT.png │ │ ├── D2.png │ │ ├── D3.png │ │ ├── D4.png │ │ ├── D5.png │ │ ├── D6.png │ │ ├── D7.png │ │ ├── D8.png │ │ ├── D9.png │ │ ├── DA.png │ │ ├── DJ.png │ │ ├── DK.png │ │ ├── DQ.png │ │ ├── DT.png │ │ ├── H2.png │ │ ├── H3.png │ │ ├── H4.png │ │ ├── H5.png │ │ ├── H6.png │ │ ├── H7.png │ │ ├── H8.png │ │ ├── H9.png │ │ ├── HA.png │ │ ├── HJ.png │ │ ├── HK.png │ │ ├── HQ.png │ │ ├── HT.png │ │ ├── S2.png │ │ ├── S3.png │ │ ├── S4.png │ │ ├── S5.png │ │ ├── S6.png │ │ ├── S7.png │ │ ├── S8.png │ │ ├── S9.png │ │ ├── SA.png │ │ ├── SJ.png │ │ ├── SK.png │ │ ├── SQ.png │ │ ├── ST.png │ │ └── card_back.png │ └── control.js ├── play_self.py ├── templates │ └── index.html └── play_against_ai_in_ui.py ├── requirements.txt ├── confs ├── nl_holdem_lgnet.py ├── nl_holdem.py ├── nl_holdem_lge.py └── nl_holdem_smlr.py ├── utils.py ├── readme.md ├── train_league.py ├── LICENSE.txt └── nl_env.ipynb /agi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/imgs/logo.png -------------------------------------------------------------------------------- /imgs/trainig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/imgs/trainig.jpg -------------------------------------------------------------------------------- /weights/c_1048.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/weights/c_1048.pkl -------------------------------------------------------------------------------- /gui/static/cards/C2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C2.png -------------------------------------------------------------------------------- /gui/static/cards/C3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C3.png -------------------------------------------------------------------------------- /gui/static/cards/C4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C4.png -------------------------------------------------------------------------------- /gui/static/cards/C5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C5.png -------------------------------------------------------------------------------- /gui/static/cards/C6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C6.png -------------------------------------------------------------------------------- /gui/static/cards/C7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C7.png -------------------------------------------------------------------------------- /gui/static/cards/C8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C8.png -------------------------------------------------------------------------------- /gui/static/cards/C9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/C9.png -------------------------------------------------------------------------------- /gui/static/cards/CA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/CA.png -------------------------------------------------------------------------------- /gui/static/cards/CJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/CJ.png -------------------------------------------------------------------------------- /gui/static/cards/CK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/CK.png -------------------------------------------------------------------------------- /gui/static/cards/CQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/CQ.png -------------------------------------------------------------------------------- /gui/static/cards/CT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/CT.png -------------------------------------------------------------------------------- /gui/static/cards/D2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D2.png -------------------------------------------------------------------------------- /gui/static/cards/D3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D3.png -------------------------------------------------------------------------------- /gui/static/cards/D4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D4.png -------------------------------------------------------------------------------- /gui/static/cards/D5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D5.png -------------------------------------------------------------------------------- /gui/static/cards/D6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D6.png -------------------------------------------------------------------------------- /gui/static/cards/D7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D7.png -------------------------------------------------------------------------------- /gui/static/cards/D8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D8.png -------------------------------------------------------------------------------- /gui/static/cards/D9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/D9.png -------------------------------------------------------------------------------- /gui/static/cards/DA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/DA.png -------------------------------------------------------------------------------- /gui/static/cards/DJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/DJ.png -------------------------------------------------------------------------------- /gui/static/cards/DK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/DK.png -------------------------------------------------------------------------------- /gui/static/cards/DQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/DQ.png -------------------------------------------------------------------------------- /gui/static/cards/DT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/DT.png -------------------------------------------------------------------------------- /gui/static/cards/H2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H2.png -------------------------------------------------------------------------------- /gui/static/cards/H3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H3.png -------------------------------------------------------------------------------- /gui/static/cards/H4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H4.png -------------------------------------------------------------------------------- /gui/static/cards/H5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H5.png -------------------------------------------------------------------------------- /gui/static/cards/H6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H6.png -------------------------------------------------------------------------------- /gui/static/cards/H7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H7.png -------------------------------------------------------------------------------- /gui/static/cards/H8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H8.png -------------------------------------------------------------------------------- /gui/static/cards/H9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/H9.png -------------------------------------------------------------------------------- /gui/static/cards/HA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/HA.png -------------------------------------------------------------------------------- /gui/static/cards/HJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/HJ.png -------------------------------------------------------------------------------- /gui/static/cards/HK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/HK.png -------------------------------------------------------------------------------- /gui/static/cards/HQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/HQ.png -------------------------------------------------------------------------------- /gui/static/cards/HT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/HT.png -------------------------------------------------------------------------------- /gui/static/cards/S2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S2.png -------------------------------------------------------------------------------- /gui/static/cards/S3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S3.png -------------------------------------------------------------------------------- /gui/static/cards/S4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S4.png -------------------------------------------------------------------------------- /gui/static/cards/S5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S5.png -------------------------------------------------------------------------------- /gui/static/cards/S6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S6.png -------------------------------------------------------------------------------- /gui/static/cards/S7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S7.png -------------------------------------------------------------------------------- /gui/static/cards/S8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S8.png -------------------------------------------------------------------------------- /gui/static/cards/S9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/S9.png -------------------------------------------------------------------------------- /gui/static/cards/SA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/SA.png -------------------------------------------------------------------------------- /gui/static/cards/SJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/SJ.png -------------------------------------------------------------------------------- /gui/static/cards/SK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/SK.png -------------------------------------------------------------------------------- /gui/static/cards/SQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/SQ.png -------------------------------------------------------------------------------- /gui/static/cards/ST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/ST.png -------------------------------------------------------------------------------- /imgs/play_against_ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/imgs/play_against_ai.jpg -------------------------------------------------------------------------------- /gui/static/cards/card_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/gui/static/cards/card_back.png -------------------------------------------------------------------------------- /agi/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/agi/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /agi/__pycache__/nl_holdem_env.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/agi/__pycache__/nl_holdem_env.cpython-37.pyc -------------------------------------------------------------------------------- /agi/__pycache__/nl_holdem_net.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/agi/__pycache__/nl_holdem_net.cpython-37.pyc -------------------------------------------------------------------------------- /agi/__pycache__/evaluation_tools.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bupticybee/AlphaNLHoldem/HEAD/agi/__pycache__/evaluation_tools.cpython-37.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tensorflow==1.15.2 2 | h5py==2.10.0 3 | numpy==1.19.4 4 | pandas==1.1.5 5 | pandocfilters==1.4.3 6 | requests==2.24.0 7 | scipy==1.5.1 8 | SuperSuit==2.3.0 9 | tabulate==0.8.7 10 | tqdm==4.54.1 11 | ray==0.8.3 12 | rlcard==1.1.0 13 | Flask==1.1.1 14 | Flask-SocketIO==4.3.1 15 | python-engineio==3.13.2 16 | python-socketio==4.6.0 17 | itsdangerous==2.0.1 18 | Jinja2==3.0.3 19 | werkzeug==2.0.3 -------------------------------------------------------------------------------- /confs/nl_holdem_lgnet.py: -------------------------------------------------------------------------------- 1 | { 2 | 'env': 'NlHoldemEnvWithOpponent', 3 | 'sample_batch_size': 50, 4 | 'train_batch_size': 1000, 5 | 'num_workers': 90, 6 | 'num_envs_per_worker': 1, 7 | #'broadcast_interval': 5, 8 | #'max_sample_requests_in_flight_per_worker': 1, 9 | #'num_data_loader_buffers': 4, 10 | 'num_gpus': 1, 11 | 'gamma': 1, 12 | 'entropy_coeff': 1e-1, 13 | 'lr': 3e-4, 14 | 'model':{ 15 | 'custom_model': 'NlHoldemLgNet', 16 | 'max_seq_len': 20, 17 | 'custom_options': { 18 | }, 19 | }, 20 | "env_config":{ 21 | 'custom_options': { 22 | "rwd_ratio": 1.0 23 | }, 24 | } 25 | } -------------------------------------------------------------------------------- /confs/nl_holdem.py: -------------------------------------------------------------------------------- 1 | { 2 | 'env': 'NlHoldemEnvWithOpponent', 3 | 'sample_batch_size': 50, 4 | 'train_batch_size': 1000, 5 | 'num_workers': 89, 6 | 'num_envs_per_worker': 1, 7 | #'broadcast_interval': 5, 8 | #'max_sample_requests_in_flight_per_worker': 1, 9 | #'num_data_loader_buffers': 4, 10 | 'num_gpus': 1, 11 | 'gamma': 1, 12 | 'entropy_coeff': 1e-1, 13 | 'lr': 3e-4, 14 | 'model':{ 15 | 'custom_model': 'NlHoldemNet', 16 | 'max_seq_len': 20, 17 | 'custom_options': { 18 | }, 19 | }, 20 | "env_config":{ 21 | 'custom_options': { 22 | 'weight':"default", 23 | "cut":[[0,12],[13,25],[26,38],[39,51],[52,53],[53,54]], 24 | 'epsilon': 0.15, 25 | 'tracker_n': 1000, 26 | 'conut_bb_rather_than_winrate': 2, 27 | 'use_history': True, 28 | 'use_cardnum': True, 29 | 'history_len': 20, 30 | }, 31 | } 32 | } -------------------------------------------------------------------------------- /confs/nl_holdem_lge.py: -------------------------------------------------------------------------------- 1 | { 2 | 'env': 'NlHoldemEnvWithOpponent', 3 | 'sample_batch_size': 50, 4 | 'train_batch_size': 1000, 5 | 'num_workers': 89, 6 | 'num_envs_per_worker': 1, 7 | #'broadcast_interval': 5, 8 | #'max_sample_requests_in_flight_per_worker': 1, 9 | #'num_data_loader_buffers': 4, 10 | 'num_gpus': 1, 11 | 'gamma': 1, 12 | 'entropy_coeff': 1.0, 13 | 'vf_loss_coeff': 1e-3, 14 | 'lr': 3e-4, 15 | 'model':{ 16 | 'custom_model': 'NlHoldemNet', 17 | 'max_seq_len': 20, 18 | 'custom_options': { 19 | }, 20 | }, 21 | "env_config":{ 22 | 'custom_options': { 23 | 'weight':"default", 24 | "cut":[[0,12],[13,25],[26,38],[39,51],[52,53],[53,54]], 25 | 'epsilon': 0.15, 26 | 'tracker_n': 1000, 27 | 'conut_bb_rather_than_winrate': 2, 28 | 'use_history': True, 29 | 'use_cardnum': True, 30 | 'history_len': 20, 31 | }, 32 | } 33 | } -------------------------------------------------------------------------------- /confs/nl_holdem_smlr.py: -------------------------------------------------------------------------------- 1 | { 2 | 'env': 'NlHoldemEnvWithOpponent', 3 | 'sample_batch_size': 50, 4 | 'train_batch_size': 1000, 5 | 'num_workers': 89, 6 | 'num_envs_per_worker': 1, 7 | #'broadcast_interval': 5, 8 | #'max_sample_requests_in_flight_per_worker': 1, 9 | #'num_data_loader_buffers': 4, 10 | 'num_gpus': 1, 11 | 'gamma': 1, 12 | 'entropy_coeff': 1e-1, 13 | 'vf_loss_coeff': 1e-3, 14 | 'lr': 3e-6, 15 | 'model':{ 16 | 'custom_model': 'NlHoldemNet', 17 | 'max_seq_len': 20, 18 | 'custom_options': { 19 | }, 20 | }, 21 | "env_config":{ 22 | 'custom_options': { 23 | 'weight':"default", 24 | "cut":[[0,12],[13,25],[26,38],[39,51],[52,53],[53,54]], 25 | 'epsilon': 0.15, 26 | 'tracker_n': 1000, 27 | 'conut_bb_rather_than_winrate': 2, 28 | 'use_history': True, 29 | 'use_cardnum': True, 30 | 'history_len': 20, 31 | }, 32 | } 33 | } -------------------------------------------------------------------------------- /gui/play_self.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("../") 3 | from ray.rllib.models import ModelCatalog 4 | from agi.nl_holdem_env import NlHoldemEnvWrapper 5 | from agi.nl_holdem_net import NlHoldemNet 6 | ModelCatalog.register_custom_model('NlHoldemNet', NlHoldemNet) 7 | import numpy as np 8 | from tqdm import tqdm 9 | import pandas as pd 10 | from agi.evaluation_tools import NNAgent,death_match 11 | 12 | #%% 13 | 14 | conf = eval(open("../confs/nl_holdem.py").read().strip()) 15 | 16 | #%% 17 | 18 | env = NlHoldemEnvWrapper( 19 | conf 20 | ) 21 | 22 | #%% 23 | 24 | i = 1048 25 | nn_agent = NNAgent(env.observation_space, 26 | env.action_space, 27 | conf, 28 | f"../weights/c_{i}.pkl", 29 | f"oppo_c{i}") 30 | 31 | #%% 32 | 33 | for i in tqdm(range(10)): 34 | obs = env.reset() 35 | d = False 36 | while not d: 37 | action_ind = nn_agent.make_action(obs) 38 | obs,r,d,i = env.step(action_ind) 39 | #break 40 | 41 | #%% 42 | 43 | print( 44 | env.env.get_state(0)["raw_obs"]["hand"],\ 45 | env.env.get_state(1)["raw_obs"]["hand"],\ 46 | env.env.get_state(1)["raw_obs"]["public_cards"],\ 47 | env.env.get_state(1)["action_record"] 48 | ) 49 | 50 | 51 | #%% 52 | 53 | print(env.env.get_payoffs()) 54 | 55 | -------------------------------------------------------------------------------- /gui/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |

Game Console.

26 | 27 |
28 | 29 | 30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /agi/evaluation_tools.py: -------------------------------------------------------------------------------- 1 | from ray.rllib.utils import try_import_tf 2 | from ray.rllib.agents.impala.vtrace_policy import VTraceTFPolicy 3 | import pandas as pd 4 | from ray.rllib.models import ModelCatalog 5 | tf = try_import_tf() 6 | from tqdm import tqdm 7 | 8 | class NNAgent(): 9 | def __init__(self,observation_space,action_space,policy_config,weights,variable_scope="oppo_policy"): 10 | self.oppo_preprocessor = ModelCatalog.get_preprocessor_for_space(observation_space, policy_config.get("model")) 11 | self.graph = tf.Graph() 12 | self.name = variable_scope 13 | with self.graph.as_default(): 14 | with tf.variable_scope(variable_scope): 15 | self.oppo_policy = VTraceTFPolicy( 16 | obs_space=self.oppo_preprocessor.observation_space, 17 | action_space=action_space, 18 | config=policy_config, 19 | ) 20 | if weights is not None: 21 | import pickle 22 | with open(weights,'rb') as fhdl: 23 | weights = pickle.load(fhdl) 24 | new_weights = {} 25 | for k,v in weights.items(): 26 | new_weights[k.replace("oppo_policy",variable_scope)] = v 27 | self.oppo_policy.set_weights(new_weights) 28 | 29 | def make_action(self,obs): 30 | observation = self.oppo_preprocessor.transform(obs) 31 | action_ind = self.oppo_policy.compute_actions([observation])[0][0] 32 | return action_ind 33 | 34 | def death_match(agent1,agent2,env): 35 | rewards = [] 36 | for i in tqdm(range(5000)): 37 | obs = env.reset() 38 | d = False 39 | while not d: 40 | legal_moves = obs["legal_moves"] 41 | #action_ind = np.random.choice(np.where(legal_moves)[0]) 42 | if env.my_agent() == 0: 43 | action_ind = agent1.make_action(obs) 44 | elif env.my_agent() == 1: 45 | action_ind = agent2.make_action(obs) 46 | else: 47 | raise 48 | obs,r,d,i = env.step(action_ind) 49 | rewards.append(r[0]) 50 | 51 | for i in tqdm(range(5000)): 52 | obs = env.reset() 53 | d = False 54 | while not d: 55 | legal_moves = obs["legal_moves"] 56 | #action_ind = np.random.choice(np.where(legal_moves)[0]) 57 | if env.my_agent() == 0: 58 | action_ind = agent2.make_action(obs) 59 | elif env.my_agent() == 1: 60 | action_ind = agent1.make_action(obs) 61 | else: 62 | raise 63 | obs,r,d,i = env.step(action_ind) 64 | rewards.append(r[1]) 65 | return rewards -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # define dataset class to feed the model 2 | import numpy as np 3 | import os 4 | import cv2 5 | import sys 6 | import time 7 | import pandas as pd 8 | import pickle 9 | from ray.tune.registry import register_trainable 10 | 11 | class ProgressBar(): 12 | def __init__(self,worksum,info="",auto_display=True): 13 | self.worksum = worksum 14 | self.info = info 15 | self.finishsum = 0 16 | self.auto_display = auto_display 17 | def startjob(self): 18 | self.begin_time = time.time() 19 | def complete(self,num): 20 | self.gaptime = time.time() - self.begin_time 21 | self.finishsum += num 22 | if self.auto_display == True: 23 | self.display_progress_bar() 24 | def display_progress_bar(self): 25 | percent = self.finishsum * 100 / self.worksum 26 | eta_time = self.gaptime * 100 / (percent + 0.001) - self.gaptime 27 | strprogress = "[" + "=" * int(percent // 2) + ">" + "-" * int(50 - percent // 2) + "]" 28 | str_log = ("%s %.2f %% %s %s/%s \t used:%ds eta:%d s" % (self.info,percent,strprogress,self.finishsum,self.worksum,self.gaptime,eta_time)) 29 | sys.stdout.write('\r' + str_log) 30 | 31 | def ma_sample(spaces): 32 | retval = {} 33 | for k,v in spaces.items(): 34 | retval[k] = v.sample() 35 | return retval 36 | 37 | def get_winrate_and_weight(logdir,league): 38 | wr_path = os.path.join(logdir,'winrates.csv') 39 | weight_path = os.path.join(logdir,'weights') 40 | 41 | wr = pd.read_csv(wr_path) 42 | winrates = wr.values[0][1:] 43 | 44 | weights = os.listdir(weight_path) 45 | weights = [i for i in weights if i.split('.')[-1] == 'pkl'] 46 | 47 | minlen = min(len(weights),len(winrates)) 48 | 49 | winrates = winrates[-minlen:] 50 | weights = weights[-minlen:] 51 | 52 | weights = sorted(weights,key=lambda x:int(x.split('.')[0].split("_")[-1])) 53 | 54 | assert(len(weights) == len(winrates)) 55 | 56 | weights = [pickle.load(open(os.path.join(weight_path,i), "rb")) for i in weights] 57 | 58 | for weight in weights: 59 | league.add_weight.remote(weight) 60 | league.set_winrates.remote(winrates) 61 | 62 | def register_restore_weight_trainer(weight): 63 | pweight = {} 64 | for k,v in weight.items(): 65 | k = k.replace("oppo_policy","default_policy") 66 | pweight[k] = v 67 | 68 | from ray.rllib.agents.impala.impala import build_trainer,DEFAULT_CONFIG,VTraceTFPolicy 69 | from ray.rllib.agents.impala.impala import validate_config,choose_policy,make_aggregators_and_optimizer 70 | from ray.rllib.agents.impala.impala import OverrideDefaultResourceRequest 71 | 72 | def my_defer_make_workers(trainer, env_creator, policy, config): 73 | def load_history(worker): 74 | for p, policy in worker.policy_map.items(): 75 | print("loading weights" + "|" * 100) 76 | policy.set_weights(pweight) 77 | 78 | # Defer worker creation to after the optimizer has been created. 79 | workers = trainer._make_workers(env_creator, policy, config, 0) 80 | print("inside my defer make workers") 81 | 82 | workers.local_worker().apply(load_history) 83 | for one_worker in workers.remote_workers(): 84 | one_worker.apply(load_history) 85 | return workers 86 | 87 | MyImpalaTrainer = build_trainer( 88 | name="IMPALA", 89 | default_config=DEFAULT_CONFIG, 90 | default_policy=VTraceTFPolicy, 91 | validate_config=validate_config, 92 | get_policy_class=choose_policy, 93 | make_workers=my_defer_make_workers, 94 | make_policy_optimizer=make_aggregators_and_optimizer, 95 | mixins=[OverrideDefaultResourceRequest]) 96 | 97 | register_trainable("IMPALA", MyImpalaTrainer) 98 | 99 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Alpha NL Holdem 2 | 3 | ![logo](imgs/logo.png) 4 | 5 | This is an implementation of a self-play non-limit texas holdem ai, using TensorFlow and ray. While heavily inspired by UCAS's work of Alpha Holdem, it's not a offical implementation of Alpha Holdem. 6 | 7 | This is a proof of concept project, rlcard's nl-holdem env was used. It's a 50bb 1v1 env, not the standard 100bb ACPC one, and bet sizes are slightly different than ACPC. 8 | 9 | I asked a few pro holdem players to play against this ai for some dozen games. They report that the ai's moves are all logical. They did not observe any significant mistakes. 10 | 11 | # Goal 12 | 1. Provide a clean codebase for apply self-play model-free RL method in Holdem-like games. 13 | 2. Try to reproduce the result of the AlphaHoldem. 14 | 3. Provide All data, including checkpoints, training methods, evaluation metrics and more. 15 | 16 | # Getting Started 17 | This project assumes you have the following: 18 | 1. Conda environment ([Anaconda](https://www.anaconda.com/) /[Miniconda](https://docs.conda.io/en/latest/miniconda.html)) 19 | 2. Python 3.7+ 20 | 21 | Install dependences: 22 | 23 | ```shell script 24 | pip3 install -r requirements.txt 25 | ``` 26 | 27 | # Usage 28 | 29 | ## Play against Neural Net 30 | 31 | First go to gui directory, and run the python script: 32 | 33 | ```shell script 34 | cd gui 35 | python3 play_against_ai_in_ui.py 36 | ``` 37 | 38 | And go to [http://localhost:8000/](http://localhost:8000/) to play against the NeuralNet. 39 | 40 | Yes, it's a small tool I write to play against AI, Yes, it looks bad. But it works. 41 | 42 | By default you are playing against an NN opponent which has been trained for about a week. 43 | 44 | ![play_against_ai](imgs/play_against_ai.jpg) 45 | 46 | ## Trainning 47 | 48 | ### Training 101 49 | 50 | To start the training, you have to first change the config in ```confs/nl_holdem.py``` 51 | 52 | By default it would require 1 GPU and 89 cpu to run this program. 53 | 54 | Modify the line in the config file: 55 | 56 | ```python 57 | 'num_workers': 89, 58 | ``` 59 | 60 | change it to the cpu core that your machine have. However you still need at least one gpu to run this the training. 61 | 62 | And then use command: 63 | ```shell script 64 | python3 train_league.py --conf confs/nl_holdem.py --sp 0.0 --upwin 1.0 --gap=500 --league_tracker_n 1000 65 | ``` 66 | 67 | Winrate against history agents will be displayed in the stdout log. 68 | 69 | ![training](imgs/trainig.jpg) 70 | 71 | ### Restore training 72 | 73 | If the training process is somehow killed or you want to start from the weights you downloaded, First put the downloaded ```league``` folder in this project's root. Then use command : 74 | 75 | ```shell script 76 | python3 train_league.py --conf confs/nl_holdem.py --sp 0.0 --upwin 1.0 --gap=500 --league_tracker_n 1000 --restore league/history_agents 77 | ``` 78 | 79 | It would auto load all training weights and continue training. 80 | 81 | # Released data 82 | 83 | 1. Weights of all checkpoints in the process of a week of training, ~ 1 billion of selfplay games: 84 | Google Drive: https://drive.google.com/file/d/1G_GwTaVe4syCwW43DauwSQi6FqjRS3nj/view?usp=sharing 85 | Baidu Drive: https://pan.baidu.com/s/1PYNLKN2CExRntVvkvYyKkA?pwd=7jmn 86 | 2. Evaluation metrics and part of results: see ```nl-evaluation.ipynb``` 87 | 88 | 89 | # Known Issues 90 | 1. Rlcard environment sucks, 50bb pot, wrong pot sizes, wrong action order after flop, I don't know where to start. But it's the only environment I konw out there suitable for this task. 91 | 2. Even after ~ 1 billion self-play, over 1000 checkpoints, the model seems still not converge, still improving itself, I really don't know when it will converge. It could be some bug, not sure. 92 | 93 | # License 94 | 95 | [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) 96 | 97 | # Warning 98 | 99 | It's illegal to use this code in any way to commercial purpose, including researching project inside a commercial entity. 100 | 101 | Especially for Chinese company JJ world(竞技世界). You'd better look elseware. 102 | 103 | -------------------------------------------------------------------------------- /agi/resnet.py: -------------------------------------------------------------------------------- 1 | from ray.rllib.models.tf.tf_modelv2 import TFModelV2 2 | 3 | from tensorflow.keras.layers import Conv2D, Dense, BatchNormalization, Activation, MaxPool2D, GlobalAveragePooling2D, Add, Input, Flatten 4 | from tensorflow.keras import Model 5 | from tensorflow.keras.regularizers import l2 6 | from ray.rllib.models.tf.visionnet_v1 import _get_filter_config 7 | from ray.rllib.models.tf.misc import normc_initializer, get_activation_fn 8 | from ray.rllib.utils import try_import_tf 9 | import numpy as np 10 | from ray.rllib.models.model import restore_original_dimensions, flatten 11 | 12 | tf = try_import_tf() 13 | 14 | class ResNet(TFModelV2): 15 | """Generic vision network implemented in ModelV2 API.""" 16 | 17 | def __init__(self, obs_space, action_space, num_outputs, model_config, 18 | name): 19 | #print("obs space in ChessNet:",obs_space) 20 | super(ResNet, self).__init__(obs_space, action_space, 21 | num_outputs, model_config, name) 22 | 23 | activation = get_activation_fn(model_config.get("conv_activation")) 24 | filters = model_config.get("conv_filters") 25 | if not filters: 26 | filters = _get_filter_config(obs_space.shape) 27 | no_final_linear = model_config.get("no_final_linear") 28 | vf_share_layers = model_config.get("vf_share_layers") 29 | 30 | inputs = tf.keras.layers.Input( 31 | shape=model_config["custom_options"]["size"], name="observations") 32 | 33 | 34 | side = tf.keras.layers.Input( 35 | shape=model_config["custom_options"]["side"], name="side") 36 | 37 | side_plat = tf.keras.layers.Lambda( 38 | lambda x: 39 | tf.broadcast_to(tf.expand_dims(tf.expand_dims(x[0],1),1), 40 | [ 41 | tf.shape(x[1])[0], 42 | tf.shape(x[1])[1], 43 | tf.shape(x[1])[2], 44 | tf.shape(x[0])[1] 45 | ]) 46 | )([side,inputs]) 47 | 48 | last_layer = tf.keras.layers.Lambda(lambda x:tf.concat([x[0],x[1]],axis=-1))([inputs,side_plat]) 49 | #last_layer = inputs 50 | 51 | # Build the action layers 52 | x = last_layer 53 | for i, (out_size, kernel, stride, blocks) in enumerate(filters[:-1], 1): 54 | """ 55 | last_layer = tf.keras.layers.Conv2D( 56 | out_size, 57 | kernel, 58 | strides=(stride, stride), 59 | activation=activation, 60 | padding="same", 61 | name="conv{}".format(i))(last_layer) 62 | """ 63 | for block in range(blocks): 64 | subsampling = stride > 1 65 | y = Conv2D(out_size, kernel_size=kernel, padding="same", strides=stride, kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="conv{}b{}".format(i,block))(x) 66 | #y = BatchNormalization()(y) 67 | y = Activation(tf.nn.relu)(y) 68 | y = Conv2D(out_size, kernel_size=kernel, padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="conv{}b{}_2".format(i,block))(y) 69 | #y = BatchNormalization()(y) 70 | if subsampling and i > 1: 71 | x = Conv2D(out_size, kernel_size=(1, 1), strides=(2, 2), padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="conv{}b{}_s".format(i,block))(x) 72 | if i > 1: 73 | x = Add()([x, y]) 74 | else: 75 | x = y 76 | x = Activation(tf.nn.relu)(x) 77 | 78 | out_size, kernel, stride,_ = filters[-1] 79 | 80 | last_layer = x 81 | 82 | last_layer = tf.keras.layers.Conv2D( 83 | out_size, 84 | kernel, 85 | strides=(stride, stride), 86 | activation=activation, 87 | padding="valid", 88 | name="conv{}".format(i + 1))(last_layer) 89 | conv_out = tf.keras.layers.Conv2D( 90 | model_config["custom_options"]["bottleneck"], 91 | [1, 1], 92 | activation=None, 93 | padding="same", 94 | name="conv_out")(last_layer) 95 | 96 | # Build the value layers 97 | last_layer = tf.keras.layers.Lambda( 98 | lambda x: tf.squeeze(x, axis=[1, 2]))(last_layer) 99 | value_out = tf.keras.layers.Dense( 100 | 1, 101 | name="value_out", 102 | activation=None, 103 | kernel_initializer=normc_initializer(0.01))(last_layer) 104 | 105 | conv_out = tf.squeeze(conv_out, axis=[1, 2]) 106 | conv_out = tf.keras.layers.Dense( 107 | num_outputs, 108 | name="conv_fuse", 109 | activation=None, 110 | kernel_initializer=normc_initializer(0.01))( 111 | conv_out 112 | ) 113 | 114 | self.base_model = tf.keras.Model([inputs,side], [conv_out, value_out]) 115 | self.register_variables(self.base_model.variables) 116 | 117 | def forward(self, input_dict, state, seq_lens): 118 | 119 | # explicit cast to float32 needed in eager 120 | model_out, self._value_out = self.base_model([ 121 | tf.cast(input_dict["obs"]["board"], tf.float32), 122 | tf.cast(input_dict["obs"]["side"], tf.float32)] 123 | ) 124 | 125 | action_mask = tf.cast(input_dict["obs"]["legal_moves"], tf.float32) 126 | 127 | inf_mask = tf.maximum(tf.log(action_mask), tf.float32.min) 128 | return model_out + inf_mask, state 129 | 130 | def value_function(self): 131 | return tf.reshape(self._value_out, [-1]) 132 | -------------------------------------------------------------------------------- /agi/league.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import ray 4 | import pandas as pd 5 | import pickle 6 | 7 | def pfsp(win_rates, weighting="squared"): 8 | win_rates = [min(i,0.95) for i in win_rates] 9 | weightings = { 10 | "variance": lambda x: x * (1 - x), 11 | "linear": lambda x: 1 - x, 12 | "linear_capped": lambda x: np.minimum(0.5, 1 - x), 13 | "squared": lambda x: (1 - x) ** 2, 14 | } 15 | fn = weightings[weighting] 16 | probs = fn(np.asarray(win_rates)) 17 | norm = probs.sum() 18 | if norm < 1e-10: 19 | return np.ones_like(win_rates) / len(win_rates) 20 | return probs / norm 21 | 22 | def kbsp(win_rates, k=5): 23 | sorted_wr = sorted(win_rates) 24 | 25 | if len(sorted_wr) < k: 26 | baseline_val = sorted_wr[-1] 27 | else: 28 | baseline_val = sorted_wr[k-1] 29 | 30 | probs = np.asarray(np.asarray(win_rates) <= baseline_val,np.float) 31 | norm = probs.sum() 32 | if norm == 0: 33 | probs = np.ones_like(win_rates) 34 | norm = probs.sum() 35 | else: 36 | probs = np.asarray(np.asarray(win_rates) <= baseline_val,np.float) 37 | norm_rest = float(probs.sum()) * 0.15 38 | 39 | z_cnt = 0 40 | for i in range(len(probs)): 41 | if probs[i] == 0: 42 | z_cnt += 1 43 | 44 | for i in range(len(probs)): 45 | if probs[i] == 0: 46 | probs[i] = norm_rest / float(z_cnt) 47 | norm = probs.sum() 48 | #print("distribute:",probs,probs / norm,norm_rest) 49 | 50 | return probs / norm 51 | 52 | class WinrateTracker(): 53 | def __init__(self,nmin=500,nmax=500): 54 | self.n = 0 55 | self.v = 0 56 | 57 | self.nmin = nmin 58 | self.nmax = nmax 59 | 60 | def update(self,v): 61 | self.n += 1 62 | self.clp_n = np.clip(self.n,self.nmin,self.nmax) 63 | self.v = self.v * (self.clp_n - 1) / self.clp_n + v / self.clp_n 64 | 65 | @ray.remote 66 | class League(): 67 | def __init__(self,initial_weight=None,n=500,last_num=1000,kbest=5,output_dir=None): 68 | self.weights_dic = {} 69 | self.current_pid = -1 70 | self.pids = [] 71 | self.winrates = None 72 | self.n = n 73 | self.last_num = last_num 74 | self.output_dir = output_dir 75 | self.kbest = kbest 76 | if initial_weight is not None: 77 | self.add_weight(initial_weight) 78 | 79 | def get_all_weights_dic(self): 80 | return self.weights_dic 81 | 82 | def get_all_policy_ids(self): 83 | pids = self.pids 84 | return pids 85 | 86 | def get_latest_policy_id(self): 87 | pid = self.pids[-1] 88 | return pid 89 | 90 | def get_weight(self,policy_id): 91 | weight = self.weights_dic[policy_id] 92 | return weight 93 | 94 | def select_opponent(self): 95 | probs = kbsp([i.v for i in self.winrates[-self.last_num:]],k=self.kbest) 96 | policy_id = np.random.choice(self.pids[-self.last_num:],p=probs) 97 | weight = self.get_weight(policy_id) 98 | return policy_id,weight 99 | 100 | def initized(self): 101 | return len(self.pids) > 0 102 | 103 | def initize_if_possible(self,new_weight): 104 | if not self.initized(): 105 | self.add_weight(new_weight) 106 | 107 | def add_weight(self,new_weight): 108 | self.current_pid += 1 109 | self.pids.append(self.current_pid) 110 | self.weights_dic[self.current_pid] = new_weight 111 | n = self.n 112 | if self.winrates is None: 113 | self.winrates = [WinrateTracker(n,n) for i in self.pids] 114 | else: 115 | old_winrates = self.winrates 116 | self.winrates = [WinrateTracker(n,n) for i in self.pids] 117 | for i in range(min(len(self.winrates),len(old_winrates))): 118 | self.winrates[i].v = old_winrates[i].v 119 | self.selfplay_winrate = WinrateTracker(n,n) 120 | 121 | 122 | output_dir = self.output_dir 123 | if output_dir: 124 | if not os.path.exists(output_dir): 125 | os.makedirs(output_dir) 126 | if not os.path.exists(os.path.join(output_dir,"weights")): 127 | os.makedirs(os.path.join(output_dir,"weights")) 128 | fname = os.path.join(output_dir, 'weights', 'c_{}.pkl'.format(self.current_pid)) 129 | with open(fname, 'wb') as whdl: 130 | pickle.dump(new_weight, whdl) 131 | 132 | 133 | def set_winrates(self,winrates): 134 | assert(len(winrates) == len(self.winrates)) 135 | for i in range(len(winrates)): 136 | wr = winrates[i] 137 | self.winrates[i].v = wr 138 | 139 | def update_result(self,policy_id,result,selfplay=False): 140 | if selfplay: 141 | self.selfplay_winrate.update(result) 142 | else: 143 | self.winrates[policy_id].update(result) 144 | 145 | def winrate_all_match(self,winrate): 146 | return np.all([i.v > winrate for i in self.winrates[-self.last_num:]]) 147 | 148 | def get_statics_table(self,dump=True): 149 | names = ["self-play",] + ["c_" + str(i) for i in self.pids] 150 | winrates = [self.selfplay_winrate.v,] + [i.v for i in self.winrates] 151 | nums = [self.selfplay_winrate.n,] + [i.n for i in self.winrates] 152 | table = pd.DataFrame({ 153 | "oppo": names, 154 | "winrate": winrates, 155 | "matches": nums, 156 | }).T 157 | 158 | output_dir = self.output_dir 159 | if output_dir: 160 | if not os.path.exists(output_dir): 161 | os.makedirs(output_dir) 162 | table.to_csv(os.path.join(output_dir,"winrates.csv"),header=False,index=False) 163 | 164 | return table -------------------------------------------------------------------------------- /gui/play_against_ai_in_ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.append("../") 4 | import tqdm 5 | import pickle 6 | import numpy as np 7 | from ray.rllib.agents.impala.vtrace_policy import VTraceTFPolicy 8 | from flask import Flask, render_template 9 | from flask_socketio import SocketIO,emit 10 | import time 11 | from threading import Thread 12 | import threading 13 | import random 14 | import json 15 | import sys 16 | sys.path.append("../") 17 | from ray.rllib.models import ModelCatalog 18 | from agi.nl_holdem_env import NlHoldemEnvWrapper 19 | from agi.nl_holdem_net import NlHoldemNet 20 | ModelCatalog.register_custom_model('NlHoldemNet', NlHoldemNet) 21 | import numpy as np 22 | from tqdm import tqdm 23 | import pandas as pd 24 | from agi.evaluation_tools import NNAgent,death_match 25 | 26 | conf = eval(open("../confs/nl_holdem.py").read().strip()) 27 | env = NlHoldemEnvWrapper( 28 | conf 29 | ) 30 | weight_index = 1048 31 | nn_agent = NNAgent(env.observation_space, 32 | env.action_space, 33 | conf, 34 | f"../weights/c_{weight_index}.pkl", 35 | f"oppo_c{weight_index}") 36 | 37 | 38 | class MyThread(): 39 | def __init__(self, args=(), kwargs=None): 40 | Thread.__init__(self, args=(), kwargs=None) 41 | self.daemon = True 42 | self.messages = [] 43 | self._stop_event = threading.Event() 44 | 45 | self.env = NlHoldemEnvWrapper( 46 | conf 47 | ) 48 | 49 | def gen_obs(self,r,d): 50 | legal_actions = [ 51 | ["Fold",0], 52 | ["Check/Call", 0], 53 | ["Raise Half Pot", 0], 54 | ["Raise Pot", 0], 55 | ["Allin", 0], 56 | ["Next Game", 0], 57 | ] 58 | 59 | hand_p0 = self.env.env.get_state(0)["raw_obs"]["hand"] 60 | hand_p1 = self.env.env.get_state(1)["raw_obs"]["hand"] 61 | public = self.env.env.get_state(1)["raw_obs"]["public_cards"] 62 | all_chip = self.env.env.get_state(1)["raw_obs"]["all_chips"] 63 | stakes = self.env.env.get_state(1)["raw_obs"]["stakes"] 64 | pot = self.env.env.get_state(1)["raw_obs"]["pot"] 65 | all_chip = [int(i) for i in all_chip] 66 | stakes = [int(i) for i in stakes] 67 | pot = int(pot) 68 | actions = self.env.env.get_state(1)["action_record"] 69 | 70 | action_recoards = [] 71 | for pid,one_action in actions: 72 | a_name = one_action.name 73 | if a_name == "CHECK_CALL": 74 | a_name = "check/call" 75 | else: 76 | a_name = a_name.replace("_"," ").lower() 77 | action_recoards.append( 78 | [int(pid), a_name] 79 | ) 80 | 81 | if d: 82 | legal_actions[-1][1] = 1 83 | payoffs = [int(i) for i in r] 84 | else: 85 | for i,one_action in enumerate(self.env.env.get_state(1)["raw_obs"]["legal_actions"]): 86 | legal_actions[one_action.value][1] = 1 87 | payoffs = [0,0] 88 | 89 | message = { 90 | "text" : "game action", 91 | "data" :{ 92 | "ai_id": self.ai_id, 93 | "hand_p0": hand_p0, 94 | "hand_p1": hand_p1, 95 | "public": public, 96 | "chip": all_chip, 97 | "stakes": stakes, 98 | "pot": pot, 99 | "legal_actions": legal_actions, 100 | "done": d, 101 | "payoffs": payoffs, 102 | "action_recoards": action_recoards, 103 | } 104 | } 105 | return message 106 | 107 | def run(self): 108 | self.ai_id = random.randint(0,1) 109 | obs = self.env.reset() 110 | d = False 111 | r = [0,0] 112 | if self.env.my_agent() == self.ai_id and not d: 113 | action_ind = nn_agent.make_action(obs) 114 | obs, r, d, i = self.env.step(action_ind) 115 | 116 | socketio.emit('message_from_server', self.gen_obs(r,d)) 117 | 118 | def send_message(self, message): 119 | action_id = message["action_id"] 120 | if action_id != 5: 121 | obs, r, d, i = self.env.step(message["action_id"]) 122 | while self.env.my_agent() == self.ai_id and not d: 123 | action_ind = nn_agent.make_action(obs) 124 | obs, r, d, i = self.env.step(action_ind) 125 | socketio.emit('message_from_server', self.gen_obs(r,d)) 126 | else: 127 | d = False 128 | r = [0,0] 129 | self.env = NlHoldemEnvWrapper( 130 | conf 131 | ) 132 | self.ai_id = random.randint(0, 1) 133 | obs = self.env.reset() 134 | if self.env.my_agent() == self.ai_id and not d: 135 | action_ind = nn_agent.make_action(obs) 136 | obs, r, d, i = self.env.step(action_ind) 137 | socketio.emit('message_from_server', self.gen_obs(r,d)) 138 | 139 | app = Flask(__name__) 140 | app.config['SECRET_KEY'] = 'secret!' 141 | socketio = SocketIO(app,logger=True, async_mode='threading', engineio_logger=False) 142 | 143 | t = None 144 | 145 | # Display the HTML Page & pass in a username parameter 146 | @app.route('/') 147 | def html(): 148 | return render_template('index.html', username="tester") 149 | 150 | # Receive a message from the front end HTML 151 | @socketio.on('send_message') 152 | def message_recieved(data): 153 | global t 154 | if data['text'] == "start": 155 | if t is None: 156 | t = MyThread() 157 | t.run() 158 | else: 159 | t = MyThread() 160 | t.run() 161 | if data['text'] == "restart": 162 | t = MyThread() 163 | t.run() 164 | elif data['text'] == "load": 165 | t.resend_last_message() 166 | else: 167 | if "action_id" in data: 168 | t.send_message(data) 169 | 170 | # Actually Start the App 171 | if __name__ == '__main__': 172 | """ Run the app. """ 173 | #import webbrowser 174 | #webbrowser.open("http://localhost:8000") 175 | socketio.run(app,host="0.0.0.0", port=8000, debug=False) 176 | -------------------------------------------------------------------------------- /agi/nl_holdem_net.py: -------------------------------------------------------------------------------- 1 | from ray.rllib.models.tf.tf_modelv2 import TFModelV2 2 | from ray.rllib.models.tf.visionnet_v1 import _get_filter_config 3 | from ray.rllib.models.tf.misc import normc_initializer, get_activation_fn 4 | from ray.rllib.utils import try_import_tf 5 | import numpy as np 6 | from ray.rllib.models.model import restore_original_dimensions, flatten 7 | 8 | from tensorflow.keras.layers import Conv2D, Dense, BatchNormalization, Activation, MaxPool2D, GlobalAveragePooling2D, Add, Input, Flatten 9 | from tensorflow.keras import Model 10 | from tensorflow.keras.regularizers import l2 11 | 12 | tf = try_import_tf() 13 | 14 | class NlHoldemNet(TFModelV2): 15 | """Generic vision network implemented in ModelV2 API.""" 16 | 17 | def __init__(self, obs_space, action_space, num_outputs, model_config, 18 | name): 19 | #print("obs space in ChessNet:",obs_space) 20 | super(NlHoldemNet, self).__init__(obs_space, action_space, 21 | num_outputs, model_config, name) 22 | 23 | input_card_info = tf.keras.layers.Input( 24 | shape=(4, 13, 6), name="card_info") 25 | 26 | input_action_info = tf.keras.layers.Input( 27 | shape=(4, 5, 25), name="action_info") 28 | 29 | input_extra_info = tf.keras.layers.Input( 30 | shape=(2,), name="extra_info") 31 | 32 | # card conv 33 | x = input_card_info 34 | for i, (out_size, kernel, stride, blocks) in enumerate([ 35 | [16, (3,3), 1, 1], 36 | [32, (3,3), 2, 2], 37 | [64, (3,3), 2, 2], 38 | ]): 39 | for block in range(blocks): 40 | subsampling = stride > 1 41 | y = Conv2D(out_size, kernel_size=kernel, padding="same", strides=stride, kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}".format(i,block))(x) 42 | #y = BatchNormalization()(y) 43 | y = Activation(tf.nn.relu)(y) 44 | y = Conv2D(out_size, kernel_size=kernel, padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}_2".format(i,block))(y) 45 | #y = BatchNormalization()(y) 46 | if subsampling and i > 1: 47 | x = Conv2D(out_size, kernel_size=(1, 1), strides=(2, 2), padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}_s".format(i,block))(x) 48 | if i > 1: 49 | x = Add()([x, y]) 50 | else: 51 | x = y 52 | x = Activation(tf.nn.relu)(x) 53 | 54 | x = Flatten()(x) 55 | last_layer_card = x 56 | 57 | # action conv 58 | x = input_action_info 59 | for i, (out_size, kernel, stride, blocks) in enumerate([ 60 | [16, (3,3), 1, 1], 61 | [32, (3,3), 2, 2], 62 | [64, (3,3), 2, 2], 63 | ]): 64 | for block in range(blocks): 65 | subsampling = stride > 1 66 | y = Conv2D(out_size, kernel_size=kernel, padding="same", strides=stride, kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}".format(i,block))(x) 67 | #y = BatchNormalization()(y) 68 | y = Activation(tf.nn.relu)(y) 69 | y = Conv2D(out_size, kernel_size=kernel, padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}_2".format(i,block))(y) 70 | #y = BatchNormalization()(y) 71 | if subsampling and i > 1: 72 | x = Conv2D(out_size, kernel_size=(1, 1), strides=(2, 2), padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}_s".format(i,block))(x) 73 | if i > 1: 74 | x = Add()([x, y]) 75 | else: 76 | x = y 77 | x = Activation(tf.nn.relu)(x) 78 | 79 | x = Flatten()(x) 80 | last_layer_history = x 81 | 82 | last_layer_extra = tf.keras.layers.Dense( 83 | 16, 84 | name="extra_fc", 85 | activation=tf.nn.relu, 86 | kernel_initializer=normc_initializer(0.01))(last_layer_card) 87 | 88 | feature_fuse = tf.keras.layers.Concatenate(axis=-1)([last_layer_card,last_layer_history,last_layer_extra]) 89 | 90 | fc_out = tf.keras.layers.Dense( 91 | 256, 92 | name="fc_1", 93 | activation=tf.nn.relu, 94 | kernel_initializer=normc_initializer(0.01))(feature_fuse) 95 | 96 | fc_out = tf.keras.layers.Dense( 97 | 128, 98 | name="fc_2", 99 | activation=tf.nn.relu, 100 | kernel_initializer=normc_initializer(0.01))(fc_out) 101 | 102 | fc_out = tf.keras.layers.Dense( 103 | 64, 104 | name="fc_3", 105 | activation=tf.nn.relu, 106 | kernel_initializer=normc_initializer(0.01))(fc_out) 107 | 108 | value_out = tf.keras.layers.Dense( 109 | 1, 110 | name="value_out", 111 | activation=None, 112 | kernel_initializer=normc_initializer(0.01))(fc_out) 113 | 114 | conv_out = tf.keras.layers.Dense( 115 | 5, 116 | name="conv_fuse", 117 | activation=None, 118 | kernel_initializer=normc_initializer(0.01))( 119 | fc_out 120 | ) 121 | 122 | self.base_model = tf.keras.Model([input_card_info,input_action_info,input_extra_info], [conv_out, value_out]) 123 | self.register_variables(self.base_model.variables) 124 | 125 | def forward(self, input_dict, state, seq_lens): 126 | 127 | # explicit cast to float32 needed in eager 128 | model_out, self._value_out = self.base_model([ 129 | tf.cast(input_dict["obs"]["card_info"], tf.float32), 130 | tf.cast(input_dict["obs"]["action_info"], tf.float32), 131 | tf.cast(input_dict["obs"]["extra_info"], tf.float32), 132 | ] 133 | ) 134 | 135 | action_mask = tf.cast(input_dict["obs"]["legal_moves"], tf.float32) 136 | 137 | inf_mask = tf.maximum(tf.log(action_mask), tf.float32.min) 138 | return model_out + inf_mask, state 139 | 140 | def value_function(self): 141 | return tf.reshape(self._value_out, [-1]) -------------------------------------------------------------------------------- /agi/nl_holdem_lg_net.py: -------------------------------------------------------------------------------- 1 | from ray.rllib.models.tf.tf_modelv2 import TFModelV2 2 | from ray.rllib.models.tf.visionnet_v1 import _get_filter_config 3 | from ray.rllib.models.tf.misc import normc_initializer, get_activation_fn 4 | from ray.rllib.utils import try_import_tf 5 | import numpy as np 6 | from ray.rllib.models.model import restore_original_dimensions, flatten 7 | 8 | from tensorflow.keras.layers import Conv2D, Dense, BatchNormalization, Activation, MaxPool2D, GlobalAveragePooling2D, Add, Input, Flatten 9 | from tensorflow.keras import Model 10 | from tensorflow.keras.regularizers import l2 11 | 12 | tf = try_import_tf() 13 | 14 | class NlHoldemLgNet(TFModelV2): 15 | """Generic vision network implemented in ModelV2 API.""" 16 | 17 | def __init__(self, obs_space, action_space, num_outputs, model_config, 18 | name): 19 | #print("obs space in ChessNet:",obs_space) 20 | super(NlHoldemLgNet, self).__init__(obs_space, action_space, 21 | num_outputs, model_config, name) 22 | 23 | input_card_info = tf.keras.layers.Input( 24 | shape=(4, 13, 6), name="card_info") 25 | 26 | input_action_info = tf.keras.layers.Input( 27 | shape=(4, 5, 25), name="action_info") 28 | 29 | input_extra_info = tf.keras.layers.Input( 30 | shape=(2,), name="extra_info") 31 | 32 | # card conv 33 | x = input_card_info 34 | for i, (out_size, kernel, stride, blocks) in enumerate([ 35 | [64, (3,3), 1, 1], 36 | [128, (3,3), 2, 2], 37 | [128, (3,3), 2, 2], 38 | ]): 39 | for block in range(blocks): 40 | subsampling = stride > 1 41 | y = Conv2D(out_size, kernel_size=kernel, padding="same", strides=stride, kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}".format(i,block))(x) 42 | #y = BatchNormalization()(y) 43 | y = Activation(tf.nn.relu)(y) 44 | y = Conv2D(out_size, kernel_size=kernel, padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}_2".format(i,block))(y) 45 | #y = BatchNormalization()(y) 46 | if subsampling and i > 1: 47 | x = Conv2D(out_size, kernel_size=(1, 1), strides=(2, 2), padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="card_conv{}b{}_s".format(i,block))(x) 48 | if i > 1: 49 | x = Add()([x, y]) 50 | else: 51 | x = y 52 | x = Activation(tf.nn.relu)(x) 53 | 54 | x = Flatten()(x) 55 | last_layer_card = x 56 | 57 | # action conv 58 | x = input_action_info 59 | for i, (out_size, kernel, stride, blocks) in enumerate([ 60 | [64, (3,3), 1, 1], 61 | [128, (3,3), 2, 2], 62 | [128, (3,3), 2, 2], 63 | ]): 64 | for block in range(blocks): 65 | subsampling = stride > 1 66 | y = Conv2D(out_size, kernel_size=kernel, padding="same", strides=stride, kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}".format(i,block))(x) 67 | #y = BatchNormalization()(y) 68 | y = Activation(tf.nn.relu)(y) 69 | y = Conv2D(out_size, kernel_size=kernel, padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}_2".format(i,block))(y) 70 | #y = BatchNormalization()(y) 71 | if subsampling and i > 1: 72 | x = Conv2D(out_size, kernel_size=(1, 1), strides=(2, 2), padding="same", kernel_initializer="he_normal", kernel_regularizer=l2(1e-4),name="history_conv{}b{}_s".format(i,block))(x) 73 | if i > 1: 74 | x = Add()([x, y]) 75 | else: 76 | x = y 77 | x = Activation(tf.nn.relu)(x) 78 | 79 | x = Flatten()(x) 80 | last_layer_history = x 81 | 82 | last_layer_extra = tf.keras.layers.Dense( 83 | 16, 84 | name="extra_fc", 85 | activation=tf.nn.relu, 86 | kernel_initializer=normc_initializer(0.01))(last_layer_card) 87 | 88 | feature_fuse = tf.keras.layers.Concatenate(axis=-1)([last_layer_card,last_layer_history,last_layer_extra]) 89 | 90 | fc_out = tf.keras.layers.Dense( 91 | 256, 92 | name="fc_1", 93 | activation=tf.nn.relu, 94 | kernel_initializer=normc_initializer(0.01))(feature_fuse) 95 | 96 | fc_out = tf.keras.layers.Dense( 97 | 128, 98 | name="fc_2", 99 | activation=tf.nn.relu, 100 | kernel_initializer=normc_initializer(0.01))(fc_out) 101 | 102 | fc_out = tf.keras.layers.Dense( 103 | 64, 104 | name="fc_3", 105 | activation=tf.nn.relu, 106 | kernel_initializer=normc_initializer(0.01))(fc_out) 107 | 108 | value_out = tf.keras.layers.Dense( 109 | 1, 110 | name="value_out", 111 | activation=None, 112 | kernel_initializer=normc_initializer(0.01))(fc_out) 113 | 114 | conv_out = tf.keras.layers.Dense( 115 | 5, 116 | name="conv_fuse", 117 | activation=None, 118 | kernel_initializer=normc_initializer(0.01))( 119 | fc_out 120 | ) 121 | 122 | self.base_model = tf.keras.Model([input_card_info,input_action_info,input_extra_info], [conv_out, value_out]) 123 | self.register_variables(self.base_model.variables) 124 | 125 | def forward(self, input_dict, state, seq_lens): 126 | 127 | # explicit cast to float32 needed in eager 128 | model_out, self._value_out = self.base_model([ 129 | tf.cast(input_dict["obs"]["card_info"], tf.float32), 130 | tf.cast(input_dict["obs"]["action_info"], tf.float32), 131 | tf.cast(input_dict["obs"]["extra_info"], tf.float32), 132 | ] 133 | ) 134 | 135 | action_mask = tf.cast(input_dict["obs"]["legal_moves"], tf.float32) 136 | 137 | inf_mask = tf.maximum(tf.log(action_mask), tf.float32.min) 138 | return model_out + inf_mask, state 139 | 140 | def value_function(self): 141 | return tf.reshape(self._value_out, [-1]) -------------------------------------------------------------------------------- /train_league.py: -------------------------------------------------------------------------------- 1 | import ray 2 | import gym 3 | import logging 4 | import argparse 5 | from matplotlib import pyplot as plt 6 | from utils import ProgressBar,ma_sample,get_winrate_and_weight,register_restore_weight_trainer 7 | #from custom_model import CustomFullyConnectedNetwork,KerasBatchNormModel,BatchNormModel,OriginalNetwork 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | from ray.rllib import agents 11 | import numpy as np 12 | import pickle 13 | from ray.rllib.utils import try_import_tf 14 | import os 15 | import pandas as pd 16 | tf = try_import_tf() 17 | 18 | from ray.rllib.agents.impala.vtrace_policy import VTraceTFPolicy 19 | from ray.rllib.agents.impala.impala import DEFAULT_CONFIG 20 | from ray.rllib.env.atari_wrappers import is_atari 21 | 22 | from ray.rllib.models import ModelCatalog 23 | from ray.tune.registry import register_env 24 | from ray import tune 25 | 26 | from agi.nl_holdem_env import NlHoldemEnvWithOpponent 27 | from agi.nl_holdem_net import NlHoldemNet 28 | from agi.nl_holdem_lg_net import NlHoldemLgNet 29 | 30 | ModelCatalog.register_custom_model('NlHoldemNet', NlHoldemNet) 31 | ModelCatalog.register_custom_model('NlHoldemLgNet', NlHoldemLgNet) 32 | 33 | from agi.league import League 34 | from ray.rllib.agents.impala.impala import ImpalaTrainer 35 | 36 | def static_vars(**kwargs): 37 | def decorate(func): 38 | for k in kwargs: 39 | setattr(func, k, kwargs[k]) 40 | return func 41 | return decorate 42 | 43 | parser = argparse.ArgumentParser() 44 | parser.add_argument('--conf', type=str) 45 | parser.add_argument('--gap', type=int, default=1000) 46 | parser.add_argument('--sp', type=float, default=0.0) 47 | parser.add_argument('--exg_oppo_prob', type=float, default=0.01) 48 | parser.add_argument('--upwin', type=float, default=1) 49 | parser.add_argument('--kbest', type=int, default=5) 50 | parser.add_argument('--league_tracker_n', type=float, default=10000) 51 | parser.add_argument('--last_num', type=int, default=100000) 52 | parser.add_argument('--rwd_update_ratio', type=float, default=1.0) 53 | parser.add_argument('--restore', type=str,default=None) 54 | parser.add_argument('--output_dir', type=str,default="league/history_agents") 55 | parser.add_argument('--mode', type=str, default="local") 56 | parser.add_argument('--experiment_name', default='run_trial_1', type=str) # please change a new name 57 | args = parser.parse_args() 58 | 59 | if args.mode == "local": 60 | ray.init() 61 | else: 62 | raise RuntimeError("unknown mode: {}".format(args.mode)) 63 | 64 | 65 | conf = eval(open(args.conf).read().strip()) 66 | 67 | register_env("NlHoldemEnvWithOpponent", lambda config: NlHoldemEnvWithOpponent( 68 | conf 69 | )) 70 | 71 | league = League.remote( 72 | n=args.league_tracker_n, 73 | last_num=args.last_num, 74 | kbest=args.kbest, 75 | output_dir=args.output_dir, 76 | ) 77 | 78 | def get_train(weight): 79 | if weight is None: 80 | pweight = None 81 | else: 82 | pweight = {} 83 | for k,v in weight.items(): 84 | k = k.replace("oppo_policy","default_policy") 85 | pweight[k] = v 86 | 87 | def train_fn_load(config, reporter): 88 | agent = ImpalaTrainer(config=config) 89 | print("LOAD: after init, before load") 90 | 91 | if pweight is not None: 92 | agent.workers.local_worker().get_policy().set_weights(pweight) 93 | agent.workers.sync_weights() 94 | 95 | print("LOAD: before train, after load") 96 | while True: 97 | result = agent.train() 98 | reporter(**result) 99 | agent.stop() 100 | 101 | return train_fn_load 102 | 103 | if args.restore is not None: 104 | get_winrate_and_weight(args.restore,league) 105 | pid = ray.get(league.get_latest_policy_id.remote()) 106 | print("latest pid: {}".format(pid)) 107 | weight = ray.get(league.get_weight.remote(pid)) 108 | #register_restore_weight_trainer(weight) 109 | train_func = get_train(weight) 110 | else: 111 | train_func = get_train(None) 112 | 113 | @static_vars(league=league) 114 | def on_episode_end(info): 115 | envs = info["env"] 116 | policies = info['policy'] 117 | default_policy = policies["default_policy"] 118 | 119 | for env in envs.vector_env.envs: 120 | if env.is_done: 121 | # 1. 更新结果到league 122 | last_reward = env.last_reward 123 | pid = env.oppo_name 124 | 125 | if np.random.random() < args.rwd_update_ratio: 126 | if pid == "self": 127 | ray.get(on_episode_end.league.update_result.remote(None,last_reward,selfplay=True)) 128 | else: 129 | ray.get(on_episode_end.league.update_result.remote(pid,last_reward,selfplay=False)) 130 | 131 | # 2. 更新对手权重 132 | 133 | # 以0.2的概率self play 134 | if np.random.random() < args.exg_oppo_prob: 135 | if np.random.random() < args.sp: 136 | p_weights = default_policy.get_weights() 137 | weight = {} 138 | for k,v in p_weights.items(): 139 | k = k.replace("default_policy","oppo_policy") 140 | weight[k] = v 141 | env.oppo_name = "self" 142 | env.oppo_policy.set_weights(weight) 143 | else: 144 | pid,weight = ray.get(on_episode_end.league.select_opponent.remote()) 145 | env.oppo_name = pid 146 | env.oppo_policy.set_weights(weight) 147 | 148 | @static_vars(league=league) 149 | def on_episode_start(info): 150 | envs = info["env"] 151 | policies = info['policy'] 152 | default_policy = policies["default_policy"] 153 | 154 | # 如果league 没有第一个权重,那么使用当前policy中的权重当作第一个 155 | if not ray.get(on_episode_start.league.initized.remote()): 156 | p_weights = default_policy.get_weights() 157 | weight = {} 158 | for k,v in p_weights.items(): 159 | k = k.replace("default_policy","oppo_policy") 160 | weight[k] = v 161 | ray.get(on_episode_start.league.initize_if_possible.remote(weight)) 162 | 163 | for env in envs.vector_env.envs: 164 | if env.oppo_name is None: 165 | pid,weight = ray.get(on_episode_start.league.select_opponent.remote()) 166 | env.oppo_name = pid 167 | env.oppo_policy.set_weights(weight) 168 | 169 | @static_vars(league=league) 170 | def on_episode_step(info): 171 | pass 172 | 173 | @static_vars(league=league,count=0) 174 | def on_train_result(info): 175 | winrates_pd = ray.get(on_train_result.league.get_statics_table.remote()) 176 | winrates_pd.to_csv("winrates.csv",header=False,index=False) 177 | 178 | table_t = winrates_pd.T 179 | table_t["mbb/h"] = np.asarray(table_t["winrate"] / 2.0 * 1000.0,np.int) 180 | info['result']['winrates'] = table_t.T 181 | on_train_result.count += 1 182 | 183 | gap = args.gap 184 | if ray.get(on_train_result.league.winrate_all_match.remote(args.upwin)) \ 185 | or on_train_result.count % gap == gap - 1: 186 | trainer = info["trainer"] 187 | p_weights = trainer.get_weights()["default_policy"] 188 | weight = {} 189 | for k,v in p_weights.items(): 190 | k = k.replace("default_policy","oppo_policy") 191 | weight[k] = v 192 | ray.get(on_train_result.league.add_weight.remote(weight)) 193 | if not os.path.exists("weights"): 194 | os.makedirs("weights") 195 | with open('output_weight.pkl','wb') as whdl: 196 | pickle.dump(weight,whdl) 197 | with open('weights/output_weight_{}.pkl'.format(on_train_result.count),'wb') as whdl: 198 | pickle.dump(weight,whdl) 199 | 200 | tune_config = { 201 | 'max_sample_requests_in_flight_per_worker': 1, 202 | 'num_data_loader_buffers': 4, 203 | "callbacks": { 204 | "on_episode_end": on_episode_end, 205 | "on_episode_start": on_episode_start, 206 | "on_episode_step": on_episode_step, 207 | "on_train_result": on_train_result, 208 | }, 209 | } 210 | 211 | tune_config.update(conf) 212 | 213 | tune.run( 214 | train_func, 215 | config=tune_config, 216 | stop={ 217 | 'timesteps_total': 10000000000, 218 | }, 219 | local_dir='log/', 220 | #resources_per_trial=ImpalaTrainer.default_resource_request, 221 | #resources_per_trial=ImpalaTrainer.default_resource_request(tune_config), 222 | resources_per_trial={'cpu':1,'gpu':1}, 223 | ) 224 | -------------------------------------------------------------------------------- /agi/nl_holdem_env.py: -------------------------------------------------------------------------------- 1 | import gym 2 | import numpy as np 3 | from gym import spaces 4 | from ray.rllib.agents.impala.vtrace_policy import VTraceTFPolicy 5 | from ray.rllib.models import ModelCatalog 6 | from ray.rllib.utils import try_import_tf 7 | import copy 8 | from io import StringIO 9 | import sys 10 | tf = try_import_tf() 11 | import rlcard 12 | from rlcard.utils import set_seed 13 | import random 14 | 15 | color2ind = dict(zip("CDHS",[0,1,2,3])) 16 | rank2ind = dict(zip("23456789TJQKA",[0,1,2,3,4,5,6,7,8,9,10,11,12])) 17 | 18 | class NlHoldemEnvWrapper(): 19 | def __init__(self,policy_config,weights=None): 20 | self.policy_config = policy_config 21 | seed = random.randint(0,1000000) 22 | self.env = rlcard.make( 23 | 'no-limit-holdem', 24 | config={ 25 | 'seed': seed, 26 | } 27 | ) 28 | set_seed(seed) 29 | self.action_num = 5 30 | 31 | 32 | space = { 33 | 'card_info': spaces.Box(low=-1024, high=1024, shape=(4,13,6)), 34 | 'action_info': spaces.Box(low=-256, high=256, shape=(4,self.action_num,4 * 6 + 1)), 35 | 'extra_info': spaces.Box(low=-256, high=256, shape=(2,)), 36 | 'legal_moves': spaces.Box( 37 | low=-1, 38 | high=1, 39 | shape=list( 40 | [self.action_num,] 41 | ) 42 | ), 43 | } 44 | 45 | self.observation_space = spaces.Dict(space) 46 | self.action_space = spaces.Discrete(self.action_num) 47 | 48 | @property 49 | def unwrapped(self): 50 | return None 51 | 52 | def _get_observation(self,obs): 53 | card_info = np.zeros([4,13,6],np.uint8) 54 | action_info = np.zeros([4,self.action_num,4 * 6 + 1],np.uint8) # 25 channel 55 | extra_info = np.zeros([2],np.uint8) # 25 channel 56 | legal_actions_info = np.zeros([self.action_num],np.uint8) # 25 channel 57 | 58 | hold_card = obs[0]["raw_obs"]["hand"] 59 | public_card = obs[0]["raw_obs"]["public_cards"] 60 | current_legal_actions = [i.value for i in obs[0]["raw_obs"]["legal_actions"]] 61 | 62 | for ind in current_legal_actions: 63 | legal_actions_info[ind] = 1 64 | 65 | flop_card = public_card[:3] 66 | turn_card = public_card[3:4] 67 | river_card = public_card[4:5] 68 | 69 | for one_card in hold_card: 70 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][0] = 1 71 | 72 | for one_card in flop_card: 73 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][1] = 1 74 | 75 | for one_card in turn_card: 76 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][2] = 1 77 | 78 | for one_card in river_card: 79 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][3] = 1 80 | 81 | for one_card in public_card: 82 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][4] = 1 83 | 84 | for one_card in public_card + hold_card: 85 | card_info[color2ind[one_card[0]]][rank2ind[one_card[1]]][5] = 1 86 | 87 | 88 | for ind_round,one_history in enumerate(self.history): 89 | for ind_h,(player_id,action_id,legal_actions) in enumerate(one_history[:6]): 90 | action_info[player_id,action_id,ind_round * 6 + ind_h] = 1 91 | action_info[2,action_id,ind_round * 6 + ind_h] = 1 92 | 93 | for la_ind in legal_actions: 94 | action_info[3,la_ind,ind_round * 6 + ind_h] = 1 95 | 96 | action_info[:,:,-1] = self.my_agent() 97 | 98 | extra_info[0] = obs[0]["raw_obs"]["stakes"][0] 99 | extra_info[1] = obs[0]["raw_obs"]["stakes"][1] 100 | 101 | return { 102 | "card_info": card_info, 103 | "action_info": action_info, 104 | "legal_moves": legal_actions_info, 105 | "extra_info": extra_info, 106 | } 107 | 108 | def _log_action(self,action_ind): 109 | self.history[ 110 | self.last_obs[0]["raw_obs"]["stage"].value 111 | ].append([ 112 | self.last_obs[0]["raw_obs"]["current_player"], 113 | action_ind, 114 | [x.value for x in self.last_obs[0]["raw_obs"]["legal_actions"]] 115 | ]) 116 | 117 | def my_agent(self): 118 | return self.env.get_player_id() 119 | 120 | def convert(self,reward): 121 | return float(reward) 122 | 123 | def step(self, action): 124 | self._log_action(action) 125 | obs = self.env.step(action) 126 | self.last_obs = obs 127 | obs = self._get_observation(obs) 128 | 129 | done = False 130 | reward = [0,0] 131 | info = {} 132 | if self.env.game.is_over(): 133 | done = True 134 | reward = list(self.env.get_payoffs()) 135 | 136 | return obs,reward,done,info 137 | 138 | def reset(self): 139 | self.history = [[],[],[],[]] 140 | obs = self.env.reset() 141 | self.last_obs = obs 142 | return self._get_observation(obs) 143 | 144 | def legal_moves(self): 145 | pass 146 | 147 | 148 | class NlHoldemEnvWithOpponent(NlHoldemEnvWrapper): 149 | def __init__(self,policy_config,weights=None,opponent="nn"): 150 | super(NlHoldemEnvWithOpponent, self).__init__(policy_config,weights) 151 | self.opponent = opponent 152 | self.rwd_ratio = policy_config["env_config"]["custom_options"].get("rwd_ratio",1) 153 | self.is_done = False 154 | if self.opponent == "nn": 155 | self.oppo_name = None 156 | self.oppo_preprocessor = ModelCatalog.get_preprocessor_for_space(self.observation_space, policy_config.get("model")) 157 | self.graph = tf.Graph() 158 | with self.graph.as_default(): 159 | with tf.variable_scope('oppo_policy'): 160 | self.oppo_policy = VTraceTFPolicy( 161 | obs_space=self.oppo_preprocessor.observation_space, 162 | action_space=self.action_space, 163 | config=policy_config, 164 | ) 165 | if weights is not None: 166 | import pickle 167 | with open(weights,'rb') as fhdl: 168 | weights = pickle.load(fhdl) 169 | self.oppo_policy.set_weights(weights) 170 | 171 | def _opponent_step(self,obs): 172 | if self.opponent == "random": 173 | rwd = [0,0] 174 | done = False 175 | info = {} 176 | while self.my_agent() != self.our_pid: 177 | legal_moves = obs["legal_moves"] 178 | action_ind = np.random.choice(np.where(legal_moves)[0]) 179 | obs,rwd,done,info = super(NlHoldemEnvWithOpponent, self).step(action_ind) 180 | if done: 181 | break 182 | return obs,rwd,done,info 183 | elif self.opponent == "nn": 184 | rwd = [0,0] 185 | done = False 186 | info = {} 187 | while self.my_agent() != self.our_pid: 188 | observation = self.oppo_preprocessor.transform(obs) 189 | action_ind = self.oppo_policy.compute_actions([observation])[0][0] 190 | obs,rwd,done,info = super(NlHoldemEnvWithOpponent, self).step(action_ind) 191 | if done: 192 | break 193 | return obs,rwd,done,info 194 | else: 195 | raise 196 | 197 | def reset(self): 198 | self.last_reward = 0 199 | self.is_done = False 200 | self.our_pid = random.randint(0,1) 201 | 202 | obs = super(NlHoldemEnvWithOpponent, self).reset() 203 | 204 | while True: 205 | obs,rwd,done,info = self._opponent_step(obs) 206 | if not done: 207 | return obs 208 | else: 209 | obs = super(NlHoldemEnvWithOpponent, self).reset() 210 | 211 | def step(self,action): 212 | obs,reward,done,info = super(NlHoldemEnvWithOpponent, self).step(action) 213 | reward = [i * self.rwd_ratio for i in reward] 214 | if done: 215 | self.is_done = True 216 | self.last_reward = reward[self.our_pid] 217 | return obs,reward[self.our_pid],done,info 218 | else: 219 | obs,reward,done,info = self._opponent_step(obs) 220 | reward = [i * self.rwd_ratio for i in reward] 221 | if done: 222 | self.is_done = True 223 | self.last_reward = reward[self.our_pid] 224 | return obs,reward[self.our_pid],done,info 225 | -------------------------------------------------------------------------------- /gui/static/control.js: -------------------------------------------------------------------------------- 1 | var socket; 2 | $(document).ready(function() { 3 | // The http vs. https is important. Use http for localhost! 4 | socket = io.connect('http://' + document.domain + ':' + location.port); 5 | 6 | // Button was clicked 7 | document.getElementById("send_button").onclick = function() { 8 | // Get the text value 9 | var text = document.getElementById("textfield_input").value; 10 | 11 | // Update the chat window 12 | document.getElementById("chat").innerHTML += "You: " + text + "\n\n"; 13 | 14 | // Emit a message to the 'send_message' socket 15 | socket.emit('send_message', {'text':text}); 16 | 17 | // Set the textfield input to empty 18 | document.getElementById("textfield_input").value = ""; 19 | } 20 | 21 | // Message recieved from server 22 | socket.on('message_from_server', function(data) { 23 | var text = data['text']; 24 | console.log(data); 25 | set_board(data["data"]) 26 | //document.getElementById("chat").innerHTML += "Server push a new state." + text + "\n"; 27 | }); 28 | 29 | socket.on('debug_info', function(data) { 30 | var text = data['text']; 31 | var input = document.getElementById("chat"); 32 | input.focus(); // that is because the suggest can be selected with mouse 33 | input.innerHTML += text + "\n"; 34 | input.scrollTop = input.scrollHeight; 35 | 36 | }); 37 | 38 | var config = { 39 | type: Phaser.AUTO, 40 | parent: 'game_container', 41 | width: 1024, 42 | height: 720, 43 | backgroundColor: '0xbababa', 44 | scene: { 45 | preload: preload, 46 | update: update_stuff 47 | } 48 | }; 49 | 50 | var game = new Phaser.Game(config); 51 | var scene; 52 | var total_reward = 0; 53 | var total_match = 0; 54 | 55 | function send_action_to_server(action_id){ 56 | socket.emit('send_message', {'text':"action_from_client","action_id":action_id}); 57 | } 58 | 59 | function onAction(action_ind){ 60 | socket.emit('send_message', {'text':"action_from_client","action_id":action_ind}); 61 | } 62 | 63 | function set_board (infos) 64 | { 65 | if (scene.reg_images){ 66 | for(var one_image of scene.reg_images){ 67 | one_image.destroy(); 68 | } 69 | } 70 | scene.reg_images = []; 71 | 72 | var done = infos["done"] 73 | console.log("done" + done) 74 | var ai_id = infos["ai_id"] 75 | var human_id = 1 - ai_id 76 | var human_hand = human_id == 0 ? infos["hand_p0"]:infos["hand_p1"] 77 | var ai_hand = ai_id == 0 ? infos["hand_p0"]:infos["hand_p1"] 78 | 79 | if (done === true){ 80 | total_reward += infos["payoffs"][human_id] 81 | total_match += 1 82 | } 83 | 84 | var x = 150; 85 | var y = 170; 86 | // opponent_hands 87 | for (var i = 0; i < 2; i++) 88 | { 89 | let one_hand = ai_hand[i]; 90 | let img_added = null; 91 | if (done === false){ 92 | img_added = scene.add.image(x, y,"card_back").setInteractive(); 93 | }else{ 94 | img_added = scene.add.image(x, y,one_hand).setInteractive(); 95 | } 96 | img_added.setScale(2); 97 | img_added.custom_info = {"what":"custom info here"}; 98 | 99 | img_added.on('pointerover',function(pointer){ 100 | img_added.setScale(3); 101 | }) 102 | img_added.on('pointerout',function(pointer){ 103 | img_added.setScale(2); 104 | }) 105 | scene.reg_images.push(img_added); 106 | x += 100; 107 | } 108 | 109 | 110 | var x = 150; 111 | var y = 550; 112 | 113 | // hero_hands 114 | for (var i = 0; i < human_hand.length; i++) 115 | { 116 | const one_hand = human_hand[i]; 117 | 118 | 119 | let img_added = scene.add.image(x, y,one_hand).setInteractive(); 120 | img_added.setScale(2); 121 | 122 | img_added.on('pointerover',function(pointer){ 123 | img_added.setScale(3); 124 | img_added.depth_bakup = img_added.depth; 125 | img_added.depth = 100; 126 | }) 127 | img_added.on('pointerout',function(pointer){ 128 | img_added.setScale(2); 129 | img_added.depth = img_added.depth_bakup; 130 | }) 131 | 132 | scene.reg_images.push(img_added); 133 | // image scale would be 86 * 117 134 | x += 100; 135 | } 136 | 137 | var x = 150; 138 | var y = 350; 139 | var public = infos["public"] 140 | 141 | // public cards 142 | for (var i = 0; i < public.length; i++) 143 | { 144 | const one_public = public[i]; 145 | 146 | 147 | let img_added = scene.add.image(x, y,one_public).setInteractive(); 148 | img_added.setScale(2); 149 | 150 | img_added.on('pointerover',function(pointer){ 151 | img_added.setScale(3); 152 | img_added.depth_bakup = img_added.depth; 153 | img_added.depth = 100; 154 | }) 155 | img_added.on('pointerout',function(pointer){ 156 | img_added.setScale(2); 157 | img_added.depth = img_added.depth_bakup; 158 | }) 159 | 160 | scene.reg_images.push(img_added); 161 | // image scale would be 86 * 117 162 | x += 100; 163 | } 164 | 165 | scene.reg_images.push(scene.add.text(170,660 , 'My stake : ' + (human_id == 0?infos["stakes"][0]:infos["stakes"][1]), { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 166 | scene.reg_images.push(scene.add.text(170,675 , 'My Chip in pot: ' + (human_id == 0?infos["chip"][0]:infos["chip"][1]), { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 167 | scene.reg_images.push(scene.add.text(170,40 , 'AI Stake: ' + (ai_id == 0?infos["stakes"][0]:infos["stakes"][1]), { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 168 | scene.reg_images.push(scene.add.text(170,55 , 'AI Chip in pot: ' + (ai_id == 0?infos["chip"][0]:infos["chip"][1]), { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 169 | scene.reg_images.push(scene.add.text(170,250 , 'Pot : ' + infos["pot"], { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 170 | scene.reg_images.push(scene.add.text(30,30 , 'Total Win : ' + total_reward, { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 171 | scene.reg_images.push(scene.add.text(30,45 , 'Total Match : ' + total_match, { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 172 | scene.reg_images.push(scene.add.text(30,60 , 'Win/Match: ' + parseInt(total_reward / total_match), { fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif' })); 173 | 174 | var x = 600; 175 | var y = 30; 176 | { 177 | for (var i = 0; i < infos["action_recoards"].length;i ++) { 178 | scene.reg_images.push(scene.add.text(x, y + i * 15, (ai_id == infos["action_recoards"][i][0]?'AI ':"Human ") + infos["action_recoards"][i][1], 179 | {fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif'})); 180 | } 181 | if(done === true){ 182 | human_payoff = infos["payoffs"][human_id] 183 | scene.reg_images.push(scene.add.text(x, y + i * 15,"Human payoff: " + human_payoff, 184 | {fontFamily: 'Georgia, "bold 32px Goudy Bookletter 1911", Times, serif'})); 185 | } 186 | } 187 | 188 | 189 | var x = 380; 190 | var y = 490; 191 | // Buttons 192 | { 193 | var legal_actions = infos["legal_actions"] 194 | for (var i = 0; i < legal_actions.length; i++) { 195 | if ( legal_actions[i][1]) { 196 | let ix = parseInt(i) 197 | let whatButton = scene.add.text(x, y + i * 40, legal_actions[i][0]) 198 | .setOrigin(0.5) 199 | .setPadding(10) 200 | .setStyle({backgroundColor: '#111'}) 201 | .setInteractive({useHandCursor: true}) 202 | .on('pointerdown', function(pointer){ 203 | onAction(ix); 204 | }) 205 | .on('pointerover', () => whatButton.setStyle({fill: '#f39c12'})) 206 | .on('pointerout', () => whatButton.setStyle({fill: '#FFF'})) 207 | scene.reg_images.push(whatButton); 208 | }else{ 209 | let whatButton = scene.add.text(x, y + i * 40, legal_actions[i][0]) 210 | .setOrigin(0.5) 211 | .setPadding(10) 212 | .setStyle({backgroundColor: '#808080'}) 213 | scene.reg_images.push(whatButton); 214 | } 215 | } 216 | } 217 | } 218 | 219 | var names = [ 220 | ] 221 | for(let i of "23456789TJQKA"){ 222 | for(let j of "CDHS"){ 223 | names.push(j + i) 224 | } 225 | } 226 | 227 | var game_object_names = [ 228 | ] 229 | 230 | function preload () 231 | { 232 | scene = this; 233 | for (var i = 0; i < names.length; i++) { 234 | var one_name = names[i]; 235 | this.load.image(one_name, "static/cards/" + one_name + ".png"); 236 | } 237 | this.load.image("card_back", "static/cards/card_back.png"); 238 | } 239 | 240 | function update_stuff (infos) 241 | { 242 | } 243 | socket.emit('send_message', {'text':"start"}); 244 | 245 | }); 246 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /nl_env.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 12, 6 | "id": "e0f2d69f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from ray.rllib.models import ModelCatalog\n", 11 | "\n", 12 | "from agi.nl_holdem_env import NlHoldemEnvWithOpponent\n", 13 | "from agi.nl_holdem_net import NlHoldemNet\n", 14 | "ModelCatalog.register_custom_model('NlHoldemNet', NlHoldemNet)\n", 15 | "import numpy as np\n", 16 | "from tqdm import tqdm\n", 17 | "from matplotlib import pyplot as plt\n", 18 | "%matplotlib inline" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "id": "7ef3a51f", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "conf = eval(open(\"confs/nl_holdem.py\").read().strip())" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "id": "1feb96c8", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "env = NlHoldemEnvWithOpponent(\n", 39 | " conf,opponent=\"nn\"\n", 40 | ")" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 104, 46 | "id": "1e2afb46", 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "name": "stderr", 51 | "output_type": "stream", 52 | "text": [ 53 | "100%|██████████| 10/10 [00:00<00:00, 641.90it/s]\n" 54 | ] 55 | } 56 | ], 57 | "source": [ 58 | "rewards = []\n", 59 | "for i in tqdm(range(10)):\n", 60 | " obs = env.reset()\n", 61 | " agid = env.our_pid\n", 62 | " #rint(\"begin with\",agid)\n", 63 | " d = False\n", 64 | " while not d:\n", 65 | " legal_moves = obs[\"legal_moves\"]\n", 66 | " action_ind = np.random.choice(np.where(legal_moves)[0])\n", 67 | " #print(\"now\",env.my_agent())\n", 68 | " assert(agid == env.my_agent())\n", 69 | " obs,r,d,i = env.step(action_ind)\n", 70 | " rewards.append(r)" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 105, 76 | "id": "a0e3bcbf", 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "data": { 81 | "text/plain": [ 82 | "9.1" 83 | ] 84 | }, 85 | "execution_count": 105, 86 | "metadata": {}, 87 | "output_type": "execute_result" 88 | } 89 | ], 90 | "source": [ 91 | "np.average(rewards)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 106, 97 | "id": "ab04906e", 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "def plot_observation(obs):\n", 102 | " card_info = obs[\"card_info\"]\n", 103 | " plt.figure(figsize=(10,2.5))\n", 104 | " for i in range(6):\n", 105 | " plt.subplot(2,3,i + 1)\n", 106 | " plt.imshow(card_info[:,:,i],vmin=0, vmax=1)\n", 107 | " \n", 108 | " action_info = obs[\"action_info\"]\n", 109 | " plt.figure(figsize=(10,7))\n", 110 | " for i in range(25):\n", 111 | " plt.subplot(5,6,i + 1)\n", 112 | " plt.imshow(action_info[:,:,i],vmin=0, vmax=1)" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 107, 118 | "id": "ad92941f", 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "data": { 123 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy0AAADaCAYAAACrb606AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAWTklEQVR4nO3dT2hcdbsH8CdpzbRKGimlScamfwRF1NcUWhOKVxEMBi+U27sq4qIUcRWFEtx00caFEFCQopR2JV2pdVMFuVQkqEXoH2gpXDe9rRSMxKR2YdIGbGtz7kLM+8b2fd9MJ2fmN2c+Hzi0mQxzHn5z8oXvnDMzLVmWZQEAAJCo1noPAAAA8K8oLQAAQNKUFgAAIGlKCwAAkDSlBQAASJrSAgAAJE1pAQAAkra8ljubm5uLiYmJaG9vj5aWllruGviLLMvi2rVrUS6Xo7W1cV6/kCOQDjkCVKOSDKlpaZmYmIienp5a7hL4N8bHx2PdunX1HmPR5AikR44A1VhMhtS0tLS3t0dExH/Ef8byuK+Wuwb+4ve4Fd/F/8z/XTYKOQLpkCNANSrJkJqWlj9PwS6P+2J5i5CAusr++KfRLo2QI5AQOQJUo4IMaZwLUAEAgKaktAAAAEm7p9Jy8ODB2LhxY6xYsSL6+/vjzJkzSz0XUGAyBKiWHIHmUnFpOXr0aAwPD8fIyEicO3cuent7Y3BwMK5cuZLHfEDByBCgWnIEmk/FpeW9996L1157LXbv3h2PP/54HD58OO6///748MMP85gPKBgZAlRLjkDzqai03Lx5M86ePRsDAwN/f4DW1hgYGIiTJ0/ecf8bN27EzMzMgg1oXpVmSIQcARaSI9CcKiotV69ejdu3b0dnZ+eC2zs7O2NycvKO+4+OjkZHR8f85oucoLlVmiERcgRYSI5Ac8r108P27t0b09PT89v4+HieuwMKSI4A1ZIj0Pgq+nLJNWvWxLJly2JqamrB7VNTU9HV1XXH/UulUpRKpeomBAqj0gyJkCPAQnIEmlNFZ1ra2tpiy5YtMTY2Nn/b3NxcjI2NxbZt25Z8OKBYZAhQLTkCzamiMy0REcPDw7Fr167YunVr9PX1xYEDB2J2djZ2796dx3xAwcgQoFpyBJpPxaVl586d8csvv8T+/ftjcnIyNm/eHMePH7/jDXEAdyNDgGrJEWg+LVmWZbXa2czMTHR0dMTz8V+xvOW+Wu0WuIvfs1vxTXwe09PTsWrVqnqPs2hyBNIhR4BqVJIhuX56GAAAQLUqvjwMmtWXE+eX5HEGy5uX5HGa3bH/+99Y1V7d6y6eCwBoDM60AAAASVNaAACApCktAABA0pQWAAAgaUoLAACQNKUFAABImtICAAAkTWkBAACSprQAAABJU1oAAICkKS0AAEDSlBYAACBpSgsAAJA0pQUAAEia0gIAACRNaQEAAJK2vN4DQKMYLG+u9wj8g/9+9G+xvOW+eo+xZL6cOF/1YzhGASgqZ1oAAICkKS0AAEDSlBYAACBpSgsAAJA0pQUAAEhaRaVldHQ0nn766Whvb4+1a9fGjh074sKFC3nNBhSQHAGqIUOgOVVUWr799tsYGhqKU6dOxVdffRW3bt2KF198MWZnZ/OaDygYOQJUQ4ZAc6roe1qOHz++4OcjR47E2rVr4+zZs/Hcc88t6WBAMckRoBoyBJpTVV8uOT09HRERq1evvuvvb9y4ETdu3Jj/eWZmpprdAQUkR4Bq/LsMiZAjUAT3/Eb8ubm52LNnTzzzzDPx5JNP3vU+o6Oj0dHRMb/19PTc86BA8cgRoBqLyZAIOQJFcM+lZWhoKL7//vv45JNP/ul99u7dG9PT0/Pb+Pj4ve4OKCA5AlRjMRkSIUegCO7p8rDXX389vvjiizhx4kSsW7fun96vVCpFqVS65+GA4pIjQDUWmyERcgSKoKLSkmVZvPHGG3Hs2LH45ptvYtOmTXnNBRSUHAGqIUOgOVVUWoaGhuKjjz6Kzz//PNrb22NycjIiIjo6OmLlypW5DAgUixwBqiFDoDlV9J6WQ4cOxfT0dDz//PPR3d09vx09ejSv+YCCkSNANWQINKeKLw8DqIYcAaohQ6A53fOnhwEAANRCVV8uSTF9OXF+SR5nsLx5SR4HmoG/l+YgX7kbxwX8e860AAAASVNaAACApCktAABA0pQWAAAgaUoLAACQNKUFAABImtICAAAkTWkBAACSprQAAABJU1oAAICkKS0AAEDSlBYAACBpSgsAAJA0pQUAAEia0gIAACRNaQEAAJK2vJY7y7IsIiJ+j1sRWS33TCVmrs0tyeP8nt1akschH7/HH8/Pn3+XjUKO0MiKlq9yZGkU7biAxaokQ1qyGibNTz/9FD09PbXaHbAI4+PjsW7dunqPsWhyBNIjR4BqLCZDalpa5ubmYmJiItrb26OlpeWu95mZmYmenp4YHx+PVatW1Wq0pmF989VI65tlWVy7di3K5XK0tjbOlaJypL6sbb4abX2LmiON9jw0Guubr0Za30oypKaXh7W2ti76lZhVq1Ylv9CNzPrmq1HWt6Ojo94jVEyOpMHa5quR1rfIOdJIz0Mjsr75apT1XWyGNM7LIgAAQFNSWgAAgKQlV1pKpVKMjIxEqVSq9yiFZH3zZX3T4HnIj7XNl/VNg+chX9Y3X0Vd35q+ER8AAKBSyZ1pAQAA+EdKCwAAkDSlBQAASJrSAgAAJE1pAQAAkpZcaTl48GBs3LgxVqxYEf39/XHmzJl6j1QIb731VrS0tCzYHnvssXqP1ZBOnDgR27dvj3K5HC0tLfHZZ58t+H2WZbF///7o7u6OlStXxsDAQFy8eLE+wzYhGZIPGbK05Eja5Eg+5MjSacYMSaq0HD16NIaHh2NkZCTOnTsXvb29MTg4GFeuXKn3aIXwxBNPxM8//zy/fffdd/UeqSHNzs5Gb29vHDx48K6/f+edd+L999+Pw4cPx+nTp+OBBx6IwcHB+O2332o8afORIfmSIUtHjqRLjuRLjiyNpsyQLCF9fX3Z0NDQ/M+3b9/OyuVyNjo6WsepimFkZCTr7e2t9xiFExHZsWPH5n+em5vLurq6snfffXf+tl9//TUrlUrZxx9/XIcJm4sMyY8MyY8cSYscyY8cyUezZEgyZ1pu3rwZZ8+ejYGBgfnbWltbY2BgIE6ePFnHyYrj4sWLUS6X4+GHH45XXnklfvzxx3qPVDiXL1+OycnJBcdxR0dH9Pf3O45zJkPyJ0NqQ47UjxzJnxzJX1EzJJnScvXq1bh9+3Z0dnYuuL2zszMmJyfrNFVx9Pf3x5EjR+L48eNx6NChuHz5cjz77LNx7dq1eo9WKH8eq47j2pMh+ZIhtSNH6keO5EuO1EZRM2R5vQegNl566aX5/z/11FPR398fGzZsiE8//TReffXVOk4GNAIZAlRLjlCNZM60rFmzJpYtWxZTU1MLbp+amoqurq46TVVcDz74YDz66KNx6dKleo9SKH8eq47j2pMhtSVD8iNH6keO1JYcyUdRMySZ0tLW1hZbtmyJsbGx+dvm5uZibGwstm3bVsfJiun69evxww8/RHd3d71HKZRNmzZFV1fXguN4ZmYmTp8+7TjOmQypLRmSHzlSP3KktuRIPoqaIUldHjY8PBy7du2KrVu3Rl9fXxw4cCBmZ2dj9+7d9R6t4b355puxffv22LBhQ0xMTMTIyEgsW7YsXn755XqP1nCuX7++4FWhy5cvx/nz52P16tWxfv362LNnT7z99tvxyCOPxKZNm2Lfvn1RLpdjx44d9Ru6SciQ/MiQpSVH0iVH8iNHlk5TZki9P77srz744INs/fr1WVtbW9bX15edOnWq3iMVws6dO7Pu7u6sra0te+ihh7KdO3dmly5dqvdYDenrr7/OIuKObdeuXVmW/fFRg/v27cs6OzuzUqmUvfDCC9mFCxfqO3QTkSH5kCFLS46kTY7kQ44snWbMkJYsy7I6dCUAAIBFSeY9LQAAAHejtAAAAElTWgAAgKQpLQAAQNKUFgAAIGlKCwAAkDSlBQAASJrSAgAAJE1pAQAAkqa0AAAASVNaAACApC2v5c7m5uZiYmIi2tvbo6WlpZa7Bv4iy7K4du1alMvlaG1tnNcv5AikQ44A1agkQ2paWiYmJqKnp6eWuwT+jfHx8Vi3bl29x1g0OQLpkSNANRaTITUtLe3t7RER8R/xn7E87qvlroG/+D1uxXfxP/N/l41CjkA65AhQjUoypKal5c9TsMvjvljeIiSgrrI//mm0SyPkCCREjgDVqCBDGucCVAAAoCkpLQAAQNLuqbQcPHgwNm7cGCtWrIj+/v44c+bMUs8FFJgMAaolR6C5VFxajh49GsPDwzEyMhLnzp2L3t7eGBwcjCtXruQxH1AwMgSolhyB5lNxaXnvvffitddei927d8fjjz8ehw8fjvvvvz8+/PDDPOYDCkaGANWSI9B8KiotN2/ejLNnz8bAwMDfH6C1NQYGBuLkyZN33P/GjRsxMzOzYAOaV6UZEiFHgIXkCDSnikrL1atX4/bt29HZ2bng9s7OzpicnLzj/qOjo9HR0TG/+SInaG6VZkiEHAEWkiPQnHL99LC9e/fG9PT0/DY+Pp7n7oACkiNAteQINL6KvlxyzZo1sWzZspiamlpw+9TUVHR1dd1x/1KpFKVSqboJgcKoNEMi5AiwkByB5lTRmZa2trbYsmVLjI2Nzd82NzcXY2NjsW3btiUfDigWGQJUS45Ac6roTEtExPDwcOzatSu2bt0afX19ceDAgZidnY3du3fnMR9QMDIEqJYcgeZTcWnZuXNn/PLLL7F///6YnJyMzZs3x/Hjx+94QxzA3cgQoFpyBJpPS5ZlWa12NjMzEx0dHfF8/Fcsb7mvVrsF7uL37FZ8E5/H9PR0rFq1qt7jLJocgXTIEaAalWRIrp8eBgAAUK2KLw8DSMGx//vfWNVe3esug+XNSzMM0JDkCI3oy4nzS/I4jXbsOtMCAAAkTWkBAACSprQAAABJU1oAAICkKS0AAEDSlBYAACBpSgsAAJA0pQUAAEia0gIAACRNaQEAAJKmtAAAAElTWgAAgKQpLQAAQNKUFgAAIGlKCwAAkDSlBQAASNryeg8A/8qXE+eX5HEGy5uX5HFIx38/+rdY3nJfvcdYMktxrDvO706O8M/IkTs5zu8upRxp1ufImRYAACBpSgsAAJA0pQUAAEia0gIAACRNaQEAAJJWUWkZHR2Np59+Otrb22Pt2rWxY8eOuHDhQl6zAQUkR4BqyBBoThWVlm+//TaGhobi1KlT8dVXX8WtW7fixRdfjNnZ2bzmAwpGjgDVkCHQnCr6npbjx48v+PnIkSOxdu3aOHv2bDz33HNLOhhQTHIEqIYMgeZU1ZdLTk9PR0TE6tWr7/r7GzduxI0bN+Z/npmZqWZ3QAHJEaAa/y5DIuQIFME9vxF/bm4u9uzZE88880w8+eSTd73P6OhodHR0zG89PT33PChQPHIEqMZiMiRCjkAR3HNpGRoaiu+//z4++eSTf3qfvXv3xvT09Pw2Pj5+r7sDCkiOANVYTIZEyBEognu6POz111+PL774Ik6cOBHr1q37p/crlUpRKpXueTiguOQIUI3FZkiEHIEiqKi0ZFkWb7zxRhw7diy++eab2LRpU15zAQUlR4BqyBBoThWVlqGhofjoo4/i888/j/b29picnIyIiI6Ojli5cmUuAwLFIkeAasgQaE4Vvafl0KFDMT09Hc8//3x0d3fPb0ePHs1rPqBg5AhQDRkCzaniy8MAqiFHgGrIEGhO9/zpYQAAALVQ1ZdLQt4Gy5vrPQL8S19OnF+Sx3Gs5yeltXW8cDeOi7tLaV1SWtuU1qWWnGkBAACSprQAAABJU1oAAICkKS0AAEDSlBYAACBpSgsAAJA0pQUAAEia0gIAACRNaQEAAJKmtAAAAElTWgAAgKQpLQAAQNKUFgAAIGlKCwAAkDSlBQAASJrSAgAAJG15LXeWZVlERPwetyKyWu4Z+Kvf41ZE/P3vslGkliMz1+aW5HF+z24tyeOQtqIdL3JkaRTtuFgq1uXuirQulWRIS1bDpPnpp5+ip6enVrsDFmF8fDzWrVtX7zEWTY5AeuQIUI3FZEhNS8vc3FxMTExEe3t7tLS03PU+MzMz0dPTE+Pj47Fq1apajdY0rG++Gml9syyLa9euRblcjtbWxrlSVI7Ul7XNV6Otb1FzpNGeh0ZjffPVSOtbSYbU9PKw1tbWRb8Ss2rVquQXupFZ33w1yvp2dHTUe4SKyZE0WNt8NdL6FjlHGul5aETWN1+Nsr6LzZDGeVkEAABoSkoLAACQtORKS6lUipGRkSiVSvUepZCsb76sbxo8D/mxtvmyvmnwPOTL+uarqOtb0zfiAwAAVCq5My0AAAD/SGkBAACSprQAAABJU1oAAICkKS0AAEDSkistBw8ejI0bN8aKFSuiv78/zpw5U++RCuGtt96KlpaWBdtjjz1W77Ea0okTJ2L79u1RLpejpaUlPvvsswW/z7Is9u/fH93d3bFy5coYGBiIixcv1mfYJiRD8iFDlpYcSZscyYccWTrNmCFJlZajR4/G8PBwjIyMxLlz56K3tzcGBwfjypUr9R6tEJ544on4+eef57fvvvuu3iM1pNnZ2ejt7Y2DBw/e9ffvvPNOvP/++3H48OE4ffp0PPDAAzE4OBi//fZbjSdtPjIkXzJk6ciRdMmRfMmRpdGUGZIlpK+vLxsaGpr/+fbt21m5XM5GR0frOFUxjIyMZL29vfUeo3AiIjt27Nj8z3Nzc1lXV1f27rvvzt/266+/ZqVSKfv444/rMGFzkSH5kSH5kSNpkSP5kSP5aJYMSeZMy82bN+Ps2bMxMDAwf1tra2sMDAzEyZMn6zhZcVy8eDHK5XI8/PDD8corr8SPP/5Y75EK5/LlyzE5ObngOO7o6Ij+/n7Hcc5kSP5kSG3IkfqRI/mTI/kraoYkU1quXr0at2/fjs7OzgW3d3Z2xuTkZJ2mKo7+/v44cuRIHD9+PA4dOhSXL1+OZ599Nq5du1bv0Qrlz2PVcVx7MiRfMqR25Ej9yJF8yZHaKGqGLK/3ANTGSy+9NP//p556Kvr7+2PDhg3x6aefxquvvlrHyYBGIEOAaskRqpHMmZY1a9bEsmXLYmpqasHtU1NT0dXVVaepiuvBBx+MRx99NC5dulTvUQrlz2PVcVx7MqS2ZEh+5Ej9yJHakiP5KGqGJFNa2traYsuWLTE2NjZ/29zcXIyNjcW2bdvqOFkxXb9+PX744Yfo7u6u9yiFsmnTpujq6lpwHM/MzMTp06cdxzmTIbUlQ/IjR+pHjtSWHMlHUTMkqcvDhoeHY9euXbF169bo6+uLAwcOxOzsbOzevbveozW8N998M7Zv3x4bNmyIiYmJGBkZiWXLlsXLL79c79EazvXr1xe8KnT58uU4f/58rF69OtavXx979uyJt99+Ox555JHYtGlT7Nu3L8rlcuzYsaN+QzcJGZIfGbK05Ei65Eh+5MjSacoMqffHl/3VBx98kK1fvz5ra2vL+vr6slOnTtV7pELYuXNn1t3dnbW1tWUPPfRQtnPnzuzSpUv1Hqshff3111lE3LHt2rUry7I/Pmpw3759WWdnZ1YqlbIXXnghu3DhQn2HbiIyJB8yZGnJkbTJkXzIkaXTjBnSkmVZVoeuBAAAsCjJvKcFAADgbpQWAAAgaUoLAACQNKUFAABImtICAAAkTWkBAACSprQAAABJU1oAAICkKS0AAEDSlBYAACBpSgsAAJC0/wfNve50P9G84wAAAABJRU5ErkJggg==\n", 124 | "text/plain": [ 125 | "
" 126 | ] 127 | }, 128 | "metadata": {}, 129 | "output_type": "display_data" 130 | }, 131 | { 132 | "data": { 133 | "image/png": "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Mjg4WMnDUQM0QZ4myNMEeZogTxNzT1M5ixcuXBjz5s2L4eHhSbcPDw/H4sWLb1nf0tISLS0t97dDkqYJ8jRBqXJ7iNBEvdMEeZpgKsp6paW5uTlWrVoV/f39E7cVi8Xo7++Prq6uGd8c6dMEeZqglB7I0wR5mmAqynqlJSJix44dsXnz5njuuedizZo18d5778WVK1diy5YtldgfNUAT5GmCUnogTxPkaYJ7KXtoefXVV+O7776Lt99+O4aGhuKZZ56JQ4cO3fLhKeYOTZCnCUrpgTxNkKcJ7qUhy7KsWgcbHR2NQqEQL8Yr0dQwv1qHZQZ8n12PI3EgRkZGor29fcYeVxO1SxPkaYI8TZCnCUqV00NFvz0MAADgfhlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApBlaAACApJU1tPT29sbq1aujra0tFi1aFJs2bYrz589Xam/UAE2QpwnyNEEpPZCnCaairKHl6NGj0dPTEydPnozDhw/H9evX46WXXoorV65Uan8kThPkaYI8TVBKD+RpgqloKmfxoUOHJl3fs2dPLFq0KAYGBuL555+f0Y1RGzRBnibI0wSl9ECeJpiKsoaWvJGRkYiIWLBgwW3//OrVq3H16tWJ66Ojo/dzOGqAJsjTBHmaoNS9eojQxFyjCW5n2h/ELxaLsX379li/fn2sXLnytmt6e3ujUChMXDo7O6e9UdKnCfI0QZ4mKDWVHiI0MZdogjuZ9tDS09MTZ8+ejb17995xzZtvvhkjIyMTl8HBwekejhqgCfI0QZ4mKDWVHiI0MZdogjuZ1tvDtm3bFgcPHoxjx47FsmXL7riupaUlWlpapr05aocmyNMEeZqg1FR7iNDEXKEJ7qasoSXLsnj99ddj//79ceTIkVixYkWl9kWN0AR5miBPE5TSA3maYCrKGlp6enri448/jgMHDkRbW1sMDQ1FREShUIgHHnigIhskbZogTxPkaYJSeiBP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\n", 134 | "text/plain": [ 135 | "
" 136 | ] 137 | }, 138 | "metadata": {}, 139 | "output_type": "display_data" 140 | } 141 | ], 142 | "source": [ 143 | "plot_observation(obs)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "id": "132cb088", 149 | "metadata": {}, 150 | "source": [ 151 | "## see how the env works" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 152, 157 | "id": "7f6a286c", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "obs = env.reset()" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 178, 167 | "id": "a253c777", 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "obs,r,d,i = env.step(1)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 179, 177 | "id": "113b5e19", 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "data": { 182 | "text/plain": [ 183 | "True" 184 | ] 185 | }, 186 | "execution_count": 179, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | } 190 | ], 191 | "source": [ 192 | "d" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 180, 198 | "id": "3e1295a7", 199 | "metadata": {}, 200 | "outputs": [ 201 | { 202 | "data": { 203 | "text/plain": [ 204 | "1" 205 | ] 206 | }, 207 | "execution_count": 180, 208 | "metadata": {}, 209 | "output_type": "execute_result" 210 | } 211 | ], 212 | "source": [ 213 | "env.our_pid" 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 181, 219 | "id": "31e77339", 220 | "metadata": {}, 221 | "outputs": [ 222 | { 223 | "data": { 224 | "image/png": "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\n", 225 | "text/plain": [ 226 | "
" 227 | ] 228 | }, 229 | "metadata": {}, 230 | "output_type": "display_data" 231 | }, 232 | { 233 | "data": { 234 | "image/png": "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\n", 235 | "text/plain": [ 236 | "
" 237 | ] 238 | }, 239 | "metadata": {}, 240 | "output_type": "display_data" 241 | } 242 | ], 243 | "source": [ 244 | "plot_observation(obs)" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": null, 250 | "id": "0c9c97fd", 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": null, 258 | "id": "9301d3d7", 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [] 262 | } 263 | ], 264 | "metadata": { 265 | "kernelspec": { 266 | "display_name": "Python 3 (ipykernel)", 267 | "language": "python", 268 | "name": "python3" 269 | }, 270 | "language_info": { 271 | "codemirror_mode": { 272 | "name": "ipython", 273 | "version": 3 274 | }, 275 | "file_extension": ".py", 276 | "mimetype": "text/x-python", 277 | "name": "python", 278 | "nbconvert_exporter": "python", 279 | "pygments_lexer": "ipython3", 280 | "version": "3.7.3" 281 | } 282 | }, 283 | "nbformat": 4, 284 | "nbformat_minor": 5 285 | } 286 | --------------------------------------------------------------------------------