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 | 
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 | 
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 | 
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": "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\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": "iVBORw0KGgoAAAANSUhEUgAAAy0AAAJCCAYAAADNxSg9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA8VklEQVR4nO3dT2yUZ57g8Z8N2E60dqURwsDiBFa7+ddESYYAYxglOVhBIoewl07voYdBu3QrMtEiTskqCurNwVJvbxSpFyXZA+YQRdA5sEh0lgg5A4gMCK2ZSEBatNKt6XXk2ARpxja0BMT17oGNKb/8cxlX+any5yOVRlU8Ve/zlr9D50f9cUOWZVkAAAAkqnG2NwAAAHA3hhYAACBphhYAACBphhYAACBphhYAACBphhYAACBphhYAACBphhYAACBp86t5sGKxGIODg9Ha2hoNDQ3VPDT3KcuyGBsbi2XLlkVj48zNupqoXZogTxPkaYI8TVCqnB6qOrQMDg5GR0dHNQ/JDBsYGIjly5fP2ONpovZpgjxNkKcJ8jRBqan0UNWhpbW1NSIi/iY2xfxYUM1Dc5++j+txIj6d+BnOlFps4sAfzlbssf/9o09V7LFnmibI0wR5miBPE5Qqp4eqDi0/vFw3PxbE/AZB1ZTsxv+Z6Zdca7GJttbKfRSsVp6DiNAEt9IEeZogTxOUKqMHH8QHAACSZmgBAACSZmgBAACSZmgBAACSNq2hZffu3bFixYpoaWmJdevWxenTp2d6X9QYTZCnCUrpgTxNkKcJ7qbsoWX//v2xc+fO2LVrV5w5cyaefvrp2LhxY1y8eLES+6MGaII8TVBKD+RpgjxNcC9lDy3vvvtubNu2LbZu3RpPPvlkfPDBB/Hggw/Gnj17KrE/aoAmyNMEpfRAnibI0wT3UtbQcu3atejv74+urq6bD9DYGF1dXXHy5Mlb1l+9ejVGR0cnXagvmiBPE5Qqt4cITdQ7TZCnCaairKHl0qVLMT4+Hu3t7ZNub29vj6GhoVvW9/T0RKFQmLh0dHTc325JjibI0wSlyu0hQhP1ThPkaYKpqOi3h7355psxMjIycRkYGKjk4agBmiBPE+RpgjxNkKeJuWd+OYsXLVoU8+bNi+Hh4Um3Dw8Px5IlS25Z39zcHM3Nzfe3Q5KmCfI0Qalye4jQRL3TBHmaYCrKeqWlqakpVq9eHX19fRO3FYvF6Ovri87OzhnfHOnTBHmaoJQeyNMEeZpgKsp6pSUiYufOnbFly5Z47rnnYu3atfHee+/FlStXYuvWrZXYHzVAE+RpglJ6IE8T5GmCeyl7aHn11Vfju+++i7fffjuGhobimWeeicOHD9/y4SnmDk2QpwlK6YE8TZCnCe6lIcuyrFoHGx0djUKhEC/GKzG/YUG1DssM+D67HkfjYIyMjERbW9uMPW4tNvHZ4JcVe+yNy56p2GPPNE2QpwnyNEGeJihVTg8V/fYwAACA+2VoAQAAkmZoAQAAklb2B/FTVanPGdTSZwyoDk3ULn9PAEBt8koLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQNEMLAACQtPmzvQGoNZ8Nflmxx9647JmKPTYAQK3ySgsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJC0soaWnp6eWLNmTbS2tsbixYtj8+bNceHChUrtjRqgCfI0QZ4mKKUH8jTBVJQ1tBw7diy6u7vj1KlTceTIkbh+/Xq89NJLceXKlUrtj8RpgjxNkKcJSumBPE0wFfPLWXz48OFJ1/fu3RuLFy+O/v7+eP7552d0Y9QGTZCnCfI0QSk9kKcJpqKsoSVvZGQkIiIWLlx42z+/evVqXL16deL66Ojo/RyOGqAJ8jRBniYoda8eIjQx12iC25n2B/GLxWLs2LEjNmzYEKtWrbrtmp6enigUChOXjo6OaW+U9GmCPE2QpwlKTaWHCE3MJZrgTqY9tHR3d8e5c+di3759d1zz5ptvxsjIyMRlYGBguoejBmiCPE2QpwlKTaWHCE3MJZrgTqb19rDt27fHoUOH4vjx47F8+fI7rmtubo7m5uZpb47aoQnyNEGeJig11R4iNDFXaIK7KWtoybIsXn/99Thw4EAcPXo0Vq5cWal9USM0QZ4myNMEpfRAniaYirKGlu7u7vj444/j4MGD0draGkNDQxERUSgU4oEHHqjIBkmbJsjTBHmaoJQeyNMEU1HWZ1ref//9GBkZiRdffDGWLl06cdm/f3+l9kfiNEGeJsjTBKX0QJ4mmIqy3x4GpTRBnibI0wSl9ECeJpiKaX97GAAAQDUYWgAAgKQZWgAAgKQZWgAAgKRN65dL3q8Dfzgbba3mpc8Gv5ztLUzZ6FgxfvRo5R5fEzdo4qZaaqKWfm6VVOkmAJi7auO/CAAAgDnL0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACTN0AIAACRtfjUPlmVZRESMXi5W87D35fvsesUee3Ssdp6HH35mP/wMZ0otNsENmiCv0k18H9cjZvahqbDv48b/hmqCH2iCUuX0UNWhZWxsLCIiHvmrf6rmYe/Tnyr2yD96tGIPXTFjY2NRKBRm9PEiaq0JSmmCvEo1cSI+nbHHpLo0QZ4mKDWVHhqymR5176JYLMbg4GC0trZGQ0PDPdePjo5GR0dHDAwMRFtbWxV2WD21dm5ZlsXY2FgsW7YsGhtn7l2F5TRRa89ZuWrt/DRRebV2fpqorFo8N01UVi2emyYqq9bOrZweqvpKS2NjYyxfvrzs+7W1tdXEEz8dtXRuM/kvIj+YThO19JxNRy2dnyaqo5bOTxOVV2vnponKq7Vz00Tl1dK5TbUHH8QHAACSZmgBAACSlvTQ0tzcHLt27Yrm5ubZ3sqMq+dzq5R6f87q/fwqod6fs3o/v0qo5+esns+tkur5eavnc6uken7e6vncqvpBfAAAgHIl/UoLAACAoQUAAEiaoQUAAEiaoQUAAEjarA4tu3fvjhUrVkRLS0usW7cuTp8+fdf1n3zySTz++OPR0tISTz31VHz66adV2ml5enp6Ys2aNdHa2hqLFy+OzZs3x4ULF+56n71790ZDQ8OkS0tLS5V2nA5N3KSJGzRxkyZu0MRNmrihHpvQw/3RxA111UQ2S/bt25c1NTVle/bsyc6fP59t27Yte+ihh7Lh4eHbrv/iiy+yefPmZb/61a+yr776KnvrrbeyBQsWZGfPnq3yzu9t48aNWW9vb3bu3Lnsyy+/zDZt2pQ9/PDD2eXLl+94n97e3qytrS379ttvJy5DQ0NV3PXs08RkmtBEniY0kaeJ+m1CD9OniZvqqYlZG1rWrl2bdXd3T1wfHx/Pli1blvX09Nx2/U9+8pPs5ZdfnnTbunXrsl/84hcV3edMuHjxYhYR2bFjx+64pre3NysUCtXbVII0MZkmNJGnCU3kaWLuNKGHqdPETfXUxKy8PezatWvR398fXV1dE7c1NjZGV1dXnDx58rb3OXny5KT1EREbN2684/qUjIyMRETEwoUL77ru8uXL8cgjj0RHR0e88sorcf78+WpsLwmauD1NaCJPE5rI08TcaEIPU6OJW9VLE7MytFy6dCnGx8ejvb190u3t7e0xNDR02/sMDQ2VtT4VxWIxduzYERs2bIhVq1bdcd1jjz0We/bsiYMHD8ZHH30UxWIx1q9fH998800Vdzt7NHErTWgiTxOayNPE3GhCD1OnicnqqYn5s72Betfd3R3nzp2LEydO3HVdZ2dndHZ2Tlxfv359PPHEE/Hhhx/GO++8U+ltUkWaIE8T5GmCUnogby42MStDy6JFi2LevHkxPDw86fbh4eFYsmTJbe+zZMmSstanYPv27XHo0KE4fvx4LF++vKz7LliwIJ599tn4+uuvK7S7tGji3jRxgyZu0sQNmrhJEzfUUxN6KI8m7q6Wm5iVt4c1NTXF6tWro6+vb+K2YrEYfX19k6bBUp2dnZPWR0QcOXLkjutnU5ZlsX379jhw4EB8/vnnsXLlyrIfY3x8PM6ePRtLly6twA7To4l704Qm8jShiTxN1E8TepgeTdxdTTcxW98AsG/fvqy5uTnbu3dv9tVXX2U///nPs4ceemjia9h+9rOfZW+88cbE+i+++CKbP39+9utf/zr7/e9/n+3atSvJr6PLsix77bXXskKhkB09enTSV8z95S9/mViTP79f/vKX2WeffZb98Y9/zPr7+7Of/vSnWUtLS3b+/PnZOIVZoQlN5GlCE3ma0ERevTahh+nTRH02MWtDS5Zl2W9+85vs4YcfzpqamrK1a9dmp06dmvizF154IduyZcuk9b/97W+zRx99NGtqasp+/OMfZ7/73e+qvOOpiYjbXnp7eyfW5M9vx44dE89Fe3t7tmnTpuzMmTPV3/ws08SWieuauEETWyaua+IGTWyZuK6JG+qxCT3cH03cUE9NNGRZllX2tRwAAIDpq+oH8YvFYgwODkZra2s0NDRU89DcpyzLYmxsLJYtWxaNjTP3UShN1C5NkKcJ8jRBniYoVU4PVR1aBgcHo6Ojo5qHZIYNDAyU/U0Vd6OJ2qcJ8jRBnibI0wSlptJDVYeW1tbWiIj4m9gU82NBNQ/Nffo+rseJ+HTiZzhTNDHZgT+crcjj/vtHn5rxx9QEeZogTxPkaYJS5fRQ1aHlh5fr5seCmN8gqJry/z/5NNMvuWpisrbWynwLeUWeW02QpwnyNEGeJihVRg+z8ntaAAAApsrQAgAAJM3QAgAAJM3QAgAAJG1aQ8vu3btjxYoV0dLSEuvWrYvTp0/P9L6oMZogTxOU0gN5miBPE9xN2UPL/v37Y+fOnbFr1644c+ZMPP3007Fx48a4ePFiJfZHDdAEeZqglB7I0wR5muBeyh5a3n333di2bVts3bo1nnzyyfjggw/iwQcfjD179lRif9QATZCnCUrpgTxNkKcJ7qWsoeXatWvR398fXV1dNx+gsTG6urri5MmTt6y/evVqjI6OTrpQXzRBniYoVW4PEZqod5ogTxNMRVlDy6VLl2J8fDza29sn3d7e3h5DQ0O3rO/p6YlCoTBx6ejouL/dkhxNkKcJSpXbQ4Qm6p0myNMEU1HRbw978803Y2RkZOIyMDBQycNRAzRBnibI0wR5miBPE3PP/HIWL1q0KObNmxfDw8OTbh8eHo4lS5bcsr65uTmam5vvb4ckTRPkaYJS5fYQoYl6pwnyNMFUlPVKS1NTU6xevTr6+vombisWi9HX1xednZ0zvjnSpwnyNEEpPZCnCfI0wVSU9UpLRMTOnTtjy5Yt8dxzz8XatWvjvffeiytXrsTWrVsrsT9qgCbI0wSl9ECeJsjTBPdS9tDy6quvxnfffRdvv/12DA0NxTPPPBOHDx++5cNTzB2aIE8TlNIDeZogTxPcS0OWZVm1DjY6OhqFQiFejFdifsOCah2WGfB9dj2OxsEYGRmJtra2GXtcTUz22eCXFXncjcuemfHH1AR5miBPE+RpglLl9FDRbw8DAAC4X4YWAAAgaYYWAAAgaYYWAAAgaWV/e1iqaukDzHAneqssf08AQG3ySgsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJC0+bO9AeCmzwa/rMjjblz2TEUeFwCgGrzSAgAAJM3QAgAAJM3QAgAAJM3QAgAAJM3QAgAAJM3QAgAAJK2soaWnpyfWrFkTra2tsXjx4ti8eXNcuHChUnujBmiCPE2QpwlK6YE8TTAVZQ0tx44di+7u7jh16lQcOXIkrl+/Hi+99FJcuXKlUvsjcZogTxPkaYJSeiBPE0xFWb9c8vDhw5Ou7927NxYvXhz9/f3x/PPPz+jGqA2aIE8T5GmCUnogTxNMxX19pmVkZCQiIhYuXDgjm6H2aYI8TZCnCUrpgTxNcDtlvdJSqlgsxo4dO2LDhg2xatWq2665evVqXL16deL66OjodA9HDdAEeZogTxOUmkoPEZqYSzTBnUz7lZbu7u44d+5c7Nu3745renp6olAoTFw6OjqmezhqgCbI0wR5mqDUVHqI0MRcognuZFpDy/bt2+PQoUPx93//97F8+fI7rnvzzTdjZGRk4jIwMDDtjZI2TZCnCfI0Qamp9hChiblCE9xNWW8Py7IsXn/99Thw4EAcPXo0Vq5cedf1zc3N0dzcfF8bJG2aIE8T5GmCUuX2EKGJeqcJpqKsoaW7uzs+/vjjOHjwYLS2tsbQ0FBERBQKhXjggQcqskHSpgnyNEGeJiilB/I0wVSU9faw999/P0ZGRuLFF1+MpUuXTlz2799fqf2ROE2QpwnyNEEpPZCnCaai7LeHQSlNkKcJ8jRBKT2Qpwmm4r5+TwsAAEClGVoAAICkGVoAAICkGVoAAICkGVoAAICklfXtYTPlwB/ORltrbcxLnw1+OdtbSMLoWDF+9GjlHr+WmqhFlehYE9VRS38HVboJAOYu/0UAAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkbX41D5ZlWUREjF4uVvOwzIAffmY//AxniiZqlyZu+j67XrHHHh2rneeh0k18H9cjZvahqbDv48b/b2iCH2iCUuX0UNWhZWxsLCIiHvmrf6rmYZlBY2NjUSgUZvTxIjRRyzQREfGnij3yjx6t2ENXTKWaOBGfzthjUl2aIE8TlJpKDw3ZTI+6d1EsFmNwcDBaW1ujoaHhnutHR0ejo6MjBgYGoq2trQo7rJ5aO7csy2JsbCyWLVsWjY0z967CcpqoteesXLV2fpqovFo7P01UVi2emyYqqxbPTROVVWvnVk4PVX2lpbGxMZYvX172/dra2mriiZ+OWjq3mfwXkR9Mp4laes6mo5bOTxPVUUvnp4nKq7Vz00Tl1dq5aaLyauncptqDD+IDAABJM7QAAABJS3poaW5ujl27dkVzc/Nsb2XG1fO5VUq9P2f1fn6VUO/PWb2fXyXU83NWz+dWSfX8vNXzuVVSPT9v9XxuVf0gPgAAQLmSfqUFAADA0AIAACTN0AIAACTN0AIAACRtVoeW3bt3x4oVK6KlpSXWrVsXp0+fvuv6Tz75JB5//PFoaWmJp556Kj799NMq7bQ8PT09sWbNmmhtbY3FixfH5s2b48KFC3e9z969e6OhoWHSpaWlpUo7TocmbtLEDZq4SRM3aOImTdxQj03o4f5o4oa6aiKbJfv27cuampqyPXv2ZOfPn8+2bduWPfTQQ9nw8PBt13/xxRfZvHnzsl/96lfZV199lb311lvZggULsrNnz1Z55/e2cePGrLe3Nzt37lz25ZdfZps2bcoefvjh7PLly3e8T29vb9bW1pZ9++23E5ehoaEq7nr2aWIyTWgiTxOayNNE/Tahh+nTxE311MSsDS1r167Nuru7J66Pj49ny5Yty3p6em67/ic/+Un28ssvT7pt3bp12S9+8YuK7nMmXLx4MYuI7NixY3dc09vbmxUKheptKkGamEwTmsjThCbyNDF3mtDD1GnipnpqYlbeHnbt2rXo7++Prq6uidsaGxujq6srTp48edv7nDx5ctL6iIiNGzfecX1KRkZGIiJi4cKFd113+fLleOSRR6KjoyNeeeWVOH/+fDW2lwRN3J4mNJGnCU3kaWJuNKGHqdHEreqliVkZWi5duhTj4+PR3t4+6fb29vYYGhq67X2GhobKWp+KYrEYO3bsiA0bNsSqVavuuO6xxx6LPXv2xMGDB+Ojjz6KYrEY69evj2+++aaKu509mriVJjSRpwlN5GlibjShh6nTxGT11MT82d5Avevu7o5z587FiRMn7rqus7MzOjs7J66vX78+nnjiifjwww/jnXfeqfQ2qSJNkKcJ8jRBKT2QNxebmJWhZdGiRTFv3rwYHh6edPvw8HAsWbLktvdZsmRJWetTsH379jh06FAcP348li9fXtZ9FyxYEM8++2x8/fXXFdpdWjRxb5q4QRM3aeIGTdykiRvqqQk9lEcTd1fLTczK28Oamppi9erV0dfXN3FbsViMvr6+SdNgqc7OzknrIyKOHDlyx/WzKcuy2L59exw4cCA+//zzWLlyZdmPMT4+HmfPno2lS5dWYIfp0cS9aUITeZrQRJ4m6qcJPUyPJu6uppuYrW8A2LdvX9bc3Jzt3bs3++qrr7Kf//zn2UMPPTTxNWw/+9nPsjfeeGNi/RdffJHNnz8/+/Wvf539/ve/z3bt2pXk19FlWZa99tprWaFQyI4ePTrpK+b+8pe/TKzJn98vf/nL7LPPPsv++Mc/Zv39/dlPf/rTrKWlJTt//vxsnMKs0IQm8jShiTxNaCKvXpvQw/Rpoj6bmLWhJcuy7De/+U328MMPZ01NTdnatWuzU6dOTfzZCy+8kG3ZsmXS+t/+9rfZo48+mjU1NWU//vGPs9/97ndV3vHURMRtL729vRNr8ue3Y8eOieeivb0927RpU3bmzJnqb36WaWLLxHVN3KCJLRPXNXGDJrZMXNfEDfXYhB7ujyZuqKcmGrIsyyr7Wg4AAMD0VfWD+MViMQYHB6O1tTUaGhqqeWjuU5ZlMTY2FsuWLYvGxpn7KJQmapcmyNMEeZogTxOUKqeHqg4tg4OD0dHRUc1DMsMGBgbK/qaKu9FE7dMEeZogTxPkaYJSU+mhqkNLa2trRET8TWyK+bGgmofmPn0f1+NEfDrxM5wpmqiOA384O+OPOXq5GI/81T9pggn+niBPE+RpglLl9FDVoeWHl+vmx4KY3yComvL/P/k00y+5aqI62lor9+3mmmCCvyfI0wR5mqBUGT3Myu9pAQAAmCpDCwAAkDRDCwAAkDRDCwAAkLRpDS27d++OFStWREtLS6xbty5Onz490/uixmiCPE1QSg/kaYI8TXA3ZQ8t+/fvj507d8auXbvizJkz8fTTT8fGjRvj4sWLldgfNUAT5GmCUnogTxPkaYJ7KXtoeffdd2Pbtm2xdevWePLJJ+ODDz6IBx98MPbs2VOJ/VEDNEGeJiilB/I0QZ4muJeyhpZr165Ff39/dHV13XyAxsbo6uqKkydP3rL+6tWrMTo6OulCfdEEeZqgVLk9RGii3mmCPE0wFWUNLZcuXYrx8fFob2+fdHt7e3sMDQ3dsr6npycKhcLEpaOj4/52S3I0QZ4mKFVuDxGaqHeaIE8TTEVFvz3szTffjJGRkYnLwMBAJQ9HDdAEeZogTxPkaYI8Tcw988tZvGjRopg3b14MDw9Pun14eDiWLFlyy/rm5uZobm6+vx2SNE2QpwlKldtDhCbqnSbI0wRTUdYrLU1NTbF69ero6+ubuK1YLEZfX190dnbO+OZInybI0wSl9ECeJsjTBFNR1istERE7d+6MLVu2xHPPPRdr166N9957L65cuRJbt26txP6oAZogTxOU0gN5miBPE9xL2UPLq6++Gt999128/fbbMTQ0FM8880wcPnz4lg9PMXdogjxNUEoP5GmCPE1wLw1ZlmXVOtjo6GgUCoV4MV6J+Q0LqnVYZsD32fU4GgdjZGQk2traZuxxNVEdnw1+OeOPOTpWjB89+idNMMHfE+RpgjxNUKqcHir67WEAAAD3y9ACAAAkzdACAAAkrewP4qeqEu/Zj4jYuOyZijwuVFMlOv4+ux4Rf5rxx60kf08AQG3ySgsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJA0QwsAAJC0+bO9AaDyPhv8csYfc3SsGD96dMYfFgDgFl5pAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAklbW0NLT0xNr1qyJ1tbWWLx4cWzevDkuXLhQqb1RAzRBnibI0wSl9ECeJpiKsoaWY8eORXd3d5w6dSqOHDkS169fj5deeimuXLlSqf2ROE2QpwnyNEEpPZCnCaZifjmLDx8+POn63r17Y/HixdHf3x/PP//8jG6M2qAJ8jRBniYopQfyNMFUlDW05I2MjERExMKFC2/751evXo2rV69OXB8dHb2fw1EDNEGeJsjTBKXu1UOEJuYaTXA70/4gfrFYjB07dsSGDRti1apVt13T09MThUJh4tLR0THtjZI+TZCnCfI0Qamp9BChiblEE9zJtIeW7u7uOHfuXOzbt++Oa958880YGRmZuAwMDEz3cNQATZCnCfI0Qamp9BChiblEE9zJtN4etn379jh06FAcP348li9ffsd1zc3N0dzcPO3NUTs0QZ4myNMEpabaQ4Qm5gpNcDdlDS1ZlsXrr78eBw4ciKNHj8bKlSsrtS9qhCbI0wR5mqCUHsjTBFNR1tDS3d0dH3/8cRw8eDBaW1tjaGgoIiIKhUI88MADFdkgadMEeZogTxOU0gN5mmAqyvpMy/vvvx8jIyPx4osvxtKlSycu+/fvr9T+SJwmyNMEeZqglB7I0wRTUfbbw6CUJsjTBHmaoJQeyNMEUzHtbw8DAACoBkMLAACQNEMLAACQNEMLAACQtGn9csn7deAPZ6OttTbmpc8Gv5ztLSRhdKwYP3q0co9fS01QHZoAAH7gvwgAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkGVoAAICkza/mwbIsi4iI0cvFah6WGfDDz+yHn+FM0UTt0sRN32fXZ3sLSfg+bjwPlWri+7geMbMPTYVpgjxNUKqcHqo6tIyNjUVExCN/9U/VPCwzaGxsLAqFwow+XoQmapkmIiL+NNsbSEqlmjgRn87YY1JdmiBPE5SaSg8N2UyPundRLBZjcHAwWltbo6Gh4Z7rR0dHo6OjIwYGBqKtra0KO6yeWju3LMtibGwsli1bFo2NM/euwnKaqLXnrFy1dn6aqLxaOz9NVFYtnpsmKqsWz00TlVVr51ZOD1V9paWxsTGWL19e9v3a2tpq4omfjlo6t5n8F5EfTKeJWnrOpqOWzk8T1VFL56eJyqu1c9NE5dXauWmi8mrp3Kbagw/iAwAASTO0AAAASUt6aGlubo5du3ZFc3PzbG9lxtXzuVVKvT9n9X5+lVDvz1m9n18l1PNzVs/nVkn1/LzV87lVUj0/b/V8blX9ID4AAEC5kn6lBQAAwNACAAAkzdACAAAkzdACAAAkbVaHlt27d8eKFSuipaUl1q1bF6dPn77r+k8++SQef/zxaGlpiaeeeio+/fTTKu20PD09PbFmzZpobW2NxYsXx+bNm+PChQt3vc/evXujoaFh0qWlpaVKO06HJm7SxA2auEkTN2jiJk3cUI9N6OH+aOKGumoimyX79u3Lmpqasj179mTnz5/Ptm3blj300EPZ8PDwbdd/8cUX2bx587Jf/epX2VdffZW99dZb2YIFC7KzZ89Weef3tnHjxqy3tzc7d+5c9uWXX2abNm3KHn744ezy5ct3vE9vb2/W1taWffvttxOXoaGhKu569mliMk1oIk8TmsjTRP02oYfp08RN9dTErA0ta9euzbq7uyeuj4+PZ8uWLct6enpuu/4nP/lJ9vLLL0+6bd26ddkvfvGLiu5zJly8eDGLiOzYsWN3XNPb25sVCoXqbSpBmphME5rI04Qm8jQxd5rQw9Rp4qZ6amJW3h527dq16O/vj66uronbGhsbo6urK06ePHnb+5w8eXLS+oiIjRs33nF9SkZGRiIiYuHChXddd/ny5XjkkUeio6MjXnnllTh//nw1tpcETdyeJjSRpwlN5GlibjShh6nRxK3qpYlZGVouXboU4+Pj0d7ePun29vb2GBoauu19hoaGylqfimKxGDt27IgNGzbEqlWr7rjuscceiz179sTBgwfjo48+imKxGOvXr49vvvmmirudPZq4lSY0kacJTeRpYm40oYep08Rk9dTE/NneQL3r7u6Oc+fOxYkTJ+66rrOzMzo7Oyeur1+/Pp544on48MMP45133qn0NqkiTZCnCfI0QSk9kDcXm5iVoWXRokUxb968GB4ennT78PBwLFmy5Lb3WbJkSVnrU7B9+/Y4dOhQHD9+PJYvX17WfRcsWBDPPvtsfP311xXaXVo0cW+auEETN2niBk3cpIkb6qkJPZRHE3dXy03MytvDmpqaYvXq1dHX1zdxW7FYjL6+vknTYKnOzs5J6yMijhw5csf1synLsti+fXscOHAgPv/881i5cmXZjzE+Ph5nz56NpUuXVmCH6dHEvWlCE3ma0ESeJuqnCT1MjyburqabmK1vANi3b1/W3Nyc7d27N/vqq6+yn//859lDDz008TVsP/vZz7I33nhjYv0XX3yRzZ8/P/v1r3+d/f73v8927dqV5NfRZVmWvfbaa1mhUMiOHj066Svm/vKXv0ysyZ/fL3/5y+yzzz7L/vjHP2b9/f3ZT3/606ylpSU7f/78bJzCrNCEJvI0oYk8TWgir16b0MP0aaI+m5i1oSXLsuw3v/lN9vDDD2dNTU3Z2rVrs1OnTk382QsvvJBt2bJl0vrf/va32aOPPpo1NTVlP/7xj7Pf/e53Vd7x1ETEbS+9vb0Ta/Lnt2PHjonnor29Pdu0aVN25syZ6m9+lmliy8R1TdygiS0T1zVxgya2TFzXxA312IQe7o8mbqinJhqyLMsq+1oOAADA9FX1g/jFYjEGBwejtbU1Ghoaqnlo7lOWZTE2NhbLli2LxsaZ+yiUJmqXJsjTBHmaIE8TlCqnh6oOLYODg9HR0VHNQzLDBgYGyv6mirvRRO3TBHmaIE8T5GmCUlPpoapDS2tra0RE/E1sivmxoJqH5j59H9fjRHw68TOcKZqoXZogTxPkaYI8TVCqnB6qOrT88HLd/FgQ8xsEVVP+/yefZvolV03UME2QpwnyNEGeJihVRg+z8ntaAAAApsrQAgAAJM3QAgAAJM3QAgAAJG1aQ8vu3btjxYoV0dLSEuvWrYvTp0/P9L6oMZogTxOU0gN5miBPE9xN2UPL/v37Y+fOnbFr1644c+ZMPP3007Fx48a4ePFiJfZHDdAEeZqglB7I0wR5muBeyh5a3n333di2bVts3bo1nnzyyfjggw/iwQcfjD179lRif9QATZCnCUrpgTxNkKcJ7qWsoeXatWvR398fXV1dNx+gsTG6urri5MmTt6y/evVqjI6OTrpQXzRBniYoVW4PEZqod5ogTxNMRVlDy6VLl2J8fDza29sn3d7e3h5DQ0O3rO/p6YlCoTBx6ejouL/dkhxNkKcJSpXbQ4Qm6p0myNMEU1HRbw978803Y2RkZOIyMDBQycNRAzRBnibI0wR5miBPE3PP/HIWL1q0KObNmxfDw8OTbh8eHo4lS5bcsr65uTmam5vvb4ckTRPkaYJS5fYQoYl6pwnyNMFUlPVKS1NTU6xevTr6+vombisWi9HX1xednZ0zvjnSpwnyNEEpPZCnCfI0wVSU9UpLRMTOnTtjy5Yt8dxzz8XatWvjvffeiytXrsTWrVsrsT9qgCbI0wSl9ECeJsjTBPdS9tDy6quvxnfffRdvv/12DA0NxTPPPBOHDx++5cNTzB2aIE8TlNIDeZogTxPcS0OWZVm1DjY6OhqFQiFejFdifsOCah2WGfB9dj2OxsEYGRmJtra2GXtcTdQuTZCnCfI0QZ4mKFVODxX99jAAAID7ZWgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSVtbQ0tPTE2vWrInW1tZYvHhxbN68OS5cuFCpvVEDNEGeJsjTBKX0QJ4mmIqyhpZjx45Fd3d3nDp1Ko4cORLXr1+Pl156Ka5cuVKp/ZE4TZCnCfI0QSk9kKcJpmJ+OYsPHz486frevXtj8eLF0d/fH88///yMbozaoAnyNEGeJiilB/I0wVTc12daRkZGIiJi4cKFM7IZap8myNMEeZqglB7I0wS3U9YrLaWKxWLs2LEjNmzYEKtWrbrtmqtXr8bVq1cnro+Ojk73cNQATZCnCfI0Qamp9BChiblEE9zJtF9p6e7ujnPnzsW+ffvuuKanpycKhcLEpaOjY7qHowZogjxNkKcJSk2lhwhNzCWa4E4asizLyr3T9u3b4+DBg3H8+PFYuXLlHdfdbgru6OiIF+OVmN+wYHo7ZlZ8n12Po3EwRkZGoq2t7ZY/18TcownyNEHe3ZqYag8RmqgnmqDUvf53o1RZbw/Lsixef/31OHDgQBw9evSeQTU3N0dzc3M5h6DGaII8TZCnCUqV20OEJuqdJpiKsoaW7u7u+Pjjj+PgwYPR2toaQ0NDERFRKBTigQceqMgGSZsmyNMEeZqglB7I0wRTUdZnWt5///0YGRmJF198MZYuXTpx2b9/f6X2R+I0QZ4myNMEpfRAniaYirLfHgalNEGeJsjTBKX0QJ4mmIr7+j0tAAAAlWZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkmZoAQAAkja/mgfLsiwiIr6P6xFZNY/M/fo+rkfEzZ/hTNFE7dIEeZogTxPkaYJS5fRQ1aFlbGwsIiJOxKfVPCwzaGxsLAqFwow+XoQmapkmyNMEeZogTxOUmkoPDdlMj7p3USwWY3BwMFpbW6OhoeGe60dHR6OjoyMGBgaira2tCjusnlo7tyzLYmxsLJYtWxaNjTP3rsJymqi156xctXZ+mqi8Wjs/TVRWLZ6bJiqrFs9NE5VVa+dWTg9VfaWlsbExli9fXvb92traauKJn45aOreZ/BeRH0yniVp6zqajls5PE9VRS+enicqrtXPTROXV2rlpovJq6dym2oMP4gMAAEkztAAAAElLemhpbm6OXbt2RXNz82xvZcbV87lVSr0/Z/V+fpVQ789ZvZ9fJdTzc1bP51ZJ9fy81fO5VVI9P2/1fG5V/SA+AABAuZJ+pQUAAMDQAgAAJM3QAgAAJM3QAgAAJG1Wh5bdu3fHihUroqWlJdatWxenT5++6/pPPvkkHn/88WhpaYmnnnoqPv300yrttDw9PT2xZs2aaG1tjcWLF8fmzZvjwoULd73P3r17o6GhYdKlpaWlSjtOhyZu0sQNmrhJEzdo4iZN3FCPTejh/mjihrpqIpsl+/bty5qamrI9e/Zk58+fz7Zt25Y99NBD2fDw8G3Xf/HFF9m8efOyX/3qV9lXX32VvfXWW9mCBQuys2fPVnnn97Zx48ast7c3O3fuXPbll19mmzZtyh5++OHs8uXLd7xPb29v1tbWln377bcTl6GhoSruevZpYjJNaCJPE5rI00T9NqGH6dPETfXUxKwNLWvXrs26u7snro+Pj2fLli3Lenp6brv+Jz/5Sfbyyy9Pum3dunXZL37xi4rucyZcvHgxi4js2LFjd1zT29ubFQqF6m0qQZqYTBOayNOEJvI0MXea0MPUaeKmempiVt4edu3atejv74+urq6J2xobG6OrqytOnjx52/ucPHly0vqIiI0bN95xfUpGRkYiImLhwoV3XXf58uV45JFHoqOjI1555ZU4f/58NbaXBE3cniY0kacJTeRpYm40oYep0cSt6qWJWRlaLl26FOPj49He3j7p9vb29hgaGrrtfYaGhspan4pisRg7duyIDRs2xKpVq+647rHHHos9e/bEwYMH46OPPopisRjr16+Pb775poq7nT2auJUmNJGnCU3kaWJuNKGHqdPEZPXUxPzZ3kC96+7ujnPnzsWJEyfuuq6zszM6Ozsnrq9fvz6eeOKJ+PDDD+Odd96p9DapIk2QpwnyNEEpPZA3F5uYlaFl0aJFMW/evBgeHp50+/DwcCxZsuS291myZElZ61Owffv2OHToUBw/fjyWL19e1n0XLFgQzz77bHz99dcV2l1aNHFvmrhBEzdp4gZN3KSJG+qpCT2URxN3V8tNzMrbw5qammL16tXR19c3cVuxWIy+vr5J02Cpzs7OSesjIo4cOXLH9bMpy7LYvn17HDhwID7//PNYuXJl2Y8xPj4eZ8+ejaVLl1Zgh+nRxL1pQhN5mtBEnibqpwk9TI8m7q6mm5itbwDYt29f1tzcnO3duzf76quvsp///OfZQw89NPE1bD/72c+yN954Y2L9F198kc2fPz/79a9/nf3+97/Pdu3aleTX0WVZlr322mtZoVDIjh49Oukr5v7yl79MrMmf3y9/+cvss88+y/74xz9m/f392U9/+tOspaUlO3/+/GycwqzQhCbyNKGJPE1oIq9em9DD9GmiPpuYtaEly7LsN7/5Tfbwww9nTU1N2dq1a7NTp05N/NkLL7yQbdmyZdL63/72t9mjjz6aNTU1ZT/+8Y+z3/3ud1Xe8dRExG0vvb29E2vy57djx46J56K9vT3btGlTdubMmepvfpZpYsvEdU3coIktE9c1cYMmtkxc18QN9diEHu6PJm6opyYasizLKvtaDgAAwPRV9YP4xWIxBgcHo7W1NRoaGqp5aO5TlmUxNjYWy5Yti8bGWfkoFAAAc1RVh5bBwcHo6Oio5iGZYQMDA2V/UwUAANyPqg4tra2tERHx5zMrou1f+df6WjJ6uRiP/NU/TfwMAQCgWqo6tPzwlrC2f9UYba2GllrkbX0AAFSbyQEAAEiaoQUAAEiaoQUAAEiaoQUAAEjatIaW3bt3x4oVK6KlpSXWrVsXp0+fnul9AQAARMQ0hpb9+/fHzp07Y9euXXHmzJl4+umnY+PGjXHx4sVK7A8AAJjjyh5a3n333di2bVts3bo1nnzyyfjggw/iwQcfjD179lRifwAAwBxX1tBy7dq16O/vj66urpsP0NgYXV1dcfLkyVvWX716NUZHRyddAAAAylHW0HLp0qUYHx+P9vb2Sbe3t7fH0NDQLet7enqiUChMXDo6Ou5vtwAAwJxT0W8Pe/PNN2NkZGTiMjAwUMnDAQAAdWh+OYsXLVoU8+bNi+Hh4Um3Dw8Px5IlS25Z39zcHM3Nzfe3QwAAYE4r65WWpqamWL16dfT19U3cViwWo6+vLzo7O2d8cwAAAGW90hIRsXPnztiyZUs899xzsXbt2njvvffiypUrsXXr1krsDwAAmOPKHlpeffXV+O677+Ltt9+OoaGheOaZZ+Lw4cO3fDgfAABgJjRkWZZV62Cjo6NRKBTin//wb6KttaLfAcAMGx0rxo8e/VOMjIxEW1vbbG8HAIA5xOQAAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkzdACAAAkrayhpaenJ9asWROtra2xePHi2Lx5c1y4cKFSewMAAChvaDl27Fh0d3fHqVOn4siRI3H9+vV46aWX4sqVK5XaHwAAMMfNL2fx4cOHJ13fu3dvLF68OPr7++P555+f0Y0BAABElDm05I2MjERExMKFC2/751evXo2rV69OXB8dHb2fwwEAAHPQtD+IXywWY8eOHbFhw4ZYtWrVbdf09PREoVCYuHR0dEx7owAAwNzUkGVZNp07vvbaa/G///f/jhMnTsTy5ctvu+Z2r7R0dHTEP//h30Rbqy8uqyWjY8X40aN/ipGRkWhra5vt7QAAMIdM6+1h27dvj0OHDsXx48fvOLBERDQ3N0dzc/O0NwcAAFDW0JJlWbz++utx4MCBOHr0aKxcubJS+wIAAIiIMoeW7u7u+Pjjj+PgwYPR2toaQ0NDERFRKBTigQceqMgGAQCAua2sz7Q0NDTc9vbe3t74u7/7u3vef3R0NAqFgs+01CCfaQEAYLaU/fYwAACAavJyBwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkDRDCwAAkLT51TxYlmURETF6uVjNwzIDfviZ/fAzBACAaqnq0DI2NhYREY/81T9V87DMoLGxsSgUCrO9DQAA5pCGrIr/dF4sFmNwcDBaW1ujoaHhnutHR0ejo6MjBgYGoq2trQo7rJ5aO7csy2JsbCyWLVsWjY3eVQgAQPVU9ZWWxsbGWL58edn3a2trq4n/sJ+OWjo3r7AAADAb/JM5AACQNEMLAACQtKSHlubm5ti1a1c0NzfP9lZmXD2fGwAAzKSqfhAfAACgXEm/0gIAAGBoAQAAkmZoAQAAkmZoAQAAkjarQ8vu3btjxYoV0dLSEuvWrYvTp0/fdf0nn3wSjz/+eLS0tMRTTz0Vn376aZV2Wp6enp5Ys2ZNtLa2xuLFi2Pz5s1x4cKFu95n79690dDQMOnS0tJSpR0DAEC6Zm1o2b9/f+zcuTN27doVZ86ciaeffjo2btwYFy9evO36f/iHf4j/8B/+Q/zH//gf4x//8R9j8+bNsXnz5jh37lyVd35vx44di+7u7jh16lQcOXIkrl+/Hi+99FJcuXLlrvdra2uLb7/9duLy5z//uUo7BgCAdM3aVx6vW7cu1qxZE//jf/yPiIgoFovR0dERr7/+erzxxhu3rH/11VfjypUrcejQoYnb/vqv/zqeeeaZ+OCDD6q27+n47rvvYvHixXHs2LF4/vnnb7tm7969sWPHjviXf/mX6m4OAAASNyuvtFy7di36+/ujq6vr5kYaG6OrqytOnjx52/ucPHly0vqIiI0bN95xfUpGRkYiImLhwoV3XXf58uV45JFHoqOjI1555ZU4f/58NbYHAABJm5Wh5dKlSzE+Ph7t7e2Tbm9vb4+hoaHb3mdoaKis9akoFouxY8eO2LBhQ6xateqO6x577LHYs2dPHDx4MD766KMoFouxfv36+Oabb6q4WwAASM/82d5Avevu7o5z587FiRMn7rqus7MzOjs7J66vX78+nnjiifjwww/jnXfeqfQ2AQAgWbMytCxatCjmzZsXw8PDk24fHh6OJUuW3PY+S5YsKWt9CrZv3x6HDh2K48ePx/Lly8u674IFC+LZZ5+Nr7/+ukK7AwCA2jArbw9ramqK1atXR19f38RtxWIx+vr6Jr3aUKqzs3PS+oiII0eO3HH9bMqyLLZv3x4HDhyIzz//PFauXFn2Y4yPj8fZs2dj6dKlFdghAADUjll7e9jOnTtjy5Yt8dxzz8XatWvjvffeiytXrsTWrVsjIuJv//Zv41//638dPT09ERHxn//zf44XXngh/vt//+/x8ssvx759++L//J//E//zf/7P2TqFO+ru7o6PP/44Dh48GK2trROfuykUCvHAAw9ExK3n91//63+Nv/7rv45/+2//bfzLv/xL/Lf/9t/iz3/+c/yn//SfZu08AAAgBbM2tLz66qvx3Xffxdtvvx1DQ0PxzDPPxOHDhyc+bP9//+//jcbGmy8ErV+/Pj7++ON466234r/8l/8S/+7f/bv4X//rf931w+2z5f3334+IiBdffHHS7b29vfF3f/d3EXHr+f3zP/9zbNu2LYaGhuJHP/pRrF69Ov7hH/4hnnzyyWptGwAAkjRrv6cFAABgKmblMy0AAABTZWgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACSZmgBAACS9v8AgW782JOmuKMAAAAASUVORK5CYII=\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 |
--------------------------------------------------------------------------------