gr-tradinggame 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gr_tradinggame/__init__.py +2 -0
- gr_tradinggame/coding/__init__.py +0 -0
- gr_tradinggame/coding/blackbox.py +50 -0
- gr_tradinggame/coding/client.py +115 -0
- gr_tradinggame/coding/gui.py +165 -0
- gr_tradinggame/coding/server.py +74 -0
- gr_tradinggame/coding/util.py +43 -0
- gr_tradinggame/manual/__init__.py +136 -0
- gr_tradinggame/py.typed +0 -0
- gr_tradinggame-0.1.13.dist-info/METADATA +43 -0
- gr_tradinggame-0.1.13.dist-info/RECORD +12 -0
- gr_tradinggame-0.1.13.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
2
|
+
import cloudpickle as pickle
|
|
3
|
+
import base64
|
|
4
|
+
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Blackbox:
|
|
9
|
+
def __init__(self, source, name='play'):
|
|
10
|
+
self.name = name
|
|
11
|
+
self.stdout = io.StringIO()
|
|
12
|
+
self.stderr = io.StringIO()
|
|
13
|
+
with redirect_stderr(self.stderr), redirect_stdout(self.stdout):
|
|
14
|
+
self.shell = TerminalInteractiveShell()
|
|
15
|
+
out = self.shell.run_cell(source)
|
|
16
|
+
if out.error_before_exec is not None:
|
|
17
|
+
raise out.error_before_exec
|
|
18
|
+
if out.error_in_exec is not None:
|
|
19
|
+
raise out.error_in_exec
|
|
20
|
+
if not isinstance(name, str) or name not in self.shell.user_ns:
|
|
21
|
+
raise Exception(f"Your source code must define a function called `{self.name}`, but only has `{self.shell.user_ns.keys()}`")
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args, **kwargs):
|
|
24
|
+
input_variable_name = '__magic_input__'
|
|
25
|
+
with redirect_stderr(self.stderr), redirect_stdout(self.stdout):
|
|
26
|
+
self.shell.user_ns[input_variable_name] = args, kwargs
|
|
27
|
+
out = self.shell.run_cell(f'{self.name}(*{input_variable_name}[0], **{input_variable_name}[1])')
|
|
28
|
+
if out.error_before_exec is not None:
|
|
29
|
+
raise out.error_before_exec
|
|
30
|
+
if out.error_in_exec is not None:
|
|
31
|
+
raise out.error_in_exec
|
|
32
|
+
return out.result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Plackbox:
|
|
36
|
+
def __init__(self, source):
|
|
37
|
+
self.foo = pickle.loads(source)
|
|
38
|
+
self.stdout = io.StringIO()
|
|
39
|
+
self.stderr = io.StringIO()
|
|
40
|
+
|
|
41
|
+
def __call__(self, *args, **kwargs):
|
|
42
|
+
with redirect_stderr(self.stderr), redirect_stdout(self.stdout):
|
|
43
|
+
return self.foo(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_function(data):
|
|
47
|
+
if data[0] == 'p':
|
|
48
|
+
return pickle.loads(base64.b64decode(data[1:]))
|
|
49
|
+
else:
|
|
50
|
+
return Blackbox(base64.b64decode(data[1:]).decode('utf-8'))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import datetime
|
|
3
|
+
import random
|
|
4
|
+
import traceback
|
|
5
|
+
import requests
|
|
6
|
+
from requests.auth import HTTPBasicAuth
|
|
7
|
+
import cloudpickle as pickle
|
|
8
|
+
import timeit
|
|
9
|
+
|
|
10
|
+
from .blackbox import Plackbox, Blackbox
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Client:
|
|
14
|
+
def __init__(self, team_name, server=None, timeout=0.1):
|
|
15
|
+
self.team_name = team_name
|
|
16
|
+
if server:
|
|
17
|
+
self.url = "https://" + server + ".ngrok-free.app"
|
|
18
|
+
else:
|
|
19
|
+
self.url = None
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
|
|
22
|
+
def test(self, source_or_function, verbose=False, strict=False, allow_state=True):
|
|
23
|
+
return self._submit(source_or_function, strict=strict, submit=False, allow_state=allow_state, verbose=verbose)
|
|
24
|
+
|
|
25
|
+
def submit(self, source_or_function, allow_state=True):
|
|
26
|
+
return self._submit(source_or_function, strict=False, submit=True, allow_state=allow_state, verbose=False)
|
|
27
|
+
|
|
28
|
+
def _submit(self, source_or_function, strict, submit, allow_state, verbose):
|
|
29
|
+
if submit and not self.url:
|
|
30
|
+
print('Automatic submission not enabled. Please send an email with your snippet to the organizers.')
|
|
31
|
+
return
|
|
32
|
+
test_cases = [
|
|
33
|
+
((1.5, 5, 1, 10, 1.3, [1.1, 2.3]), "generic inputs", None),
|
|
34
|
+
((-1.5, 5, 1, 10, 1.3, [1.1, 2.3]), "negative reward", (False, 'It never makes sense to take a negative reward')),
|
|
35
|
+
((0, 5, 1, 10, 1.3, [1.1, 2.3]), "zero rewards", (False, "It doesn't make sense to accept a lockout in return for no reward")),
|
|
36
|
+
((1.5, 5, 1, 10, 1.3, []), "playing by yourself", None),
|
|
37
|
+
((1.5, 5, 1, 10, -1, [-1.1, 2.3]), "negative score", None),
|
|
38
|
+
((0.0001, 5, 10, 10, 1.3, [1.1, 2.3]), "no time left after this round", (True, 'There is no time left after this round, you should accept any reward you can get')),
|
|
39
|
+
((0.0001, 0, 1, 10, 1.3, [1.1, 2.3]), "no lockout", (True, 'There is no lockout, you should accept any reward you can get')),
|
|
40
|
+
]
|
|
41
|
+
if isinstance(source_or_function, str):
|
|
42
|
+
output = source_or_function
|
|
43
|
+
foo = Blackbox(output)
|
|
44
|
+
else:
|
|
45
|
+
output = pickle.dumps(source_or_function)
|
|
46
|
+
foo = Plackbox(output)
|
|
47
|
+
outputs = dict()
|
|
48
|
+
for j in range(3):
|
|
49
|
+
for i, (args, err, hint) in enumerate(test_cases):
|
|
50
|
+
test_str = f'Test {i} ("{err}"): f{args}'
|
|
51
|
+
if j == 0 and verbose:
|
|
52
|
+
print(test_str)
|
|
53
|
+
test_str = ''
|
|
54
|
+
try:
|
|
55
|
+
a = foo(*args)
|
|
56
|
+
if j == 0:
|
|
57
|
+
outputs[i] = a
|
|
58
|
+
else:
|
|
59
|
+
if a != outputs[i] and not allow_state:
|
|
60
|
+
print(test_str)
|
|
61
|
+
raise Exception(f"""You previously returned `{outputs[i]}` and are now returning `{a}`. If this is intentional, set `allow_state=True`""")
|
|
62
|
+
elif not isinstance(a, bool):
|
|
63
|
+
print(test_str)
|
|
64
|
+
raise Exception(f"""You should return `False` or `True` but you returned `{a}` of type {type(a)}""")
|
|
65
|
+
elif strict and hint is not None and a != hint[0]:
|
|
66
|
+
print(test_str)
|
|
67
|
+
raise Exception(hint[1])
|
|
68
|
+
except Exception:
|
|
69
|
+
print(test_str)
|
|
70
|
+
raise
|
|
71
|
+
if j == 0 and verbose:
|
|
72
|
+
print(f'\tSuccess -- returned {a}')
|
|
73
|
+
tic = timeit.default_timer()
|
|
74
|
+
for j in range(1_000):
|
|
75
|
+
T = int(1_000_000 * random.random())
|
|
76
|
+
t = int(random.random() * T)
|
|
77
|
+
lockout = int(10 * random.random())
|
|
78
|
+
reward = 100 * random.random()
|
|
79
|
+
score = reward * T * random.random()
|
|
80
|
+
scores = [reward * T * random.random() for _ in range(10)]
|
|
81
|
+
args = (reward, lockout, t, T, score, scores)
|
|
82
|
+
try:
|
|
83
|
+
a = foo(*args)
|
|
84
|
+
except Exception:
|
|
85
|
+
print(f'Random input {args}')
|
|
86
|
+
raise
|
|
87
|
+
else:
|
|
88
|
+
if not isinstance(a, bool):
|
|
89
|
+
print(f'Random input {args}')
|
|
90
|
+
raise Exception(f"""You should return `False` or `True` but returned `{a}` of type {type(a)}""")
|
|
91
|
+
toc = timeit.default_timer()
|
|
92
|
+
if toc - tic > max(1, (j + 1) * self.timeout):
|
|
93
|
+
raise Exception(f"""Your code is too slow. Should take {self.timeout}s per round but took {(toc - tic) / (j + 1):.3f}s""")
|
|
94
|
+
if submit:
|
|
95
|
+
self.fancy_submit(source_or_function)
|
|
96
|
+
else:
|
|
97
|
+
print("All tests passed. You're ready to call `submit`\n")
|
|
98
|
+
|
|
99
|
+
def fancy_submit(self, output):
|
|
100
|
+
if isinstance(output, str):
|
|
101
|
+
submission = 's' + base64.b64encode(output.encode('utf-8')).decode('utf-8')
|
|
102
|
+
else:
|
|
103
|
+
submission = 'p' + base64.b64encode(pickle.dumps(output)).decode('utf-8')
|
|
104
|
+
j = {"team": self.team_name, 'time': str(datetime.datetime.now()), 'submission': submission}
|
|
105
|
+
response = requests.post(
|
|
106
|
+
self.url + '/receive',
|
|
107
|
+
json=j,
|
|
108
|
+
auth=HTTPBasicAuth("colabuser", "secretcolab"),
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
except Exception:
|
|
113
|
+
raise Exception("Couldn't connect to server. Please notify an organizer.")
|
|
114
|
+
else:
|
|
115
|
+
print('Submission successful: ', j['team'], j['time'])
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import timeit
|
|
2
|
+
import numpy as np
|
|
3
|
+
import ipywidgets as widgets
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from IPython.display import clear_output, display
|
|
7
|
+
import time
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from .blackbox import generate_function
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CodingGame:
|
|
14
|
+
def __init__(
|
|
15
|
+
self, rounds=2000, lockout=5, plot_frequency=0.3, plot_width=1000, length_in_seconds=30, eliminate_slow_teams=False,
|
|
16
|
+
functions=None, submissions=None, additional_functions=None, random_draw_function='a', must_be_only_team=False
|
|
17
|
+
):
|
|
18
|
+
if functions is None and submissions is None:
|
|
19
|
+
with open("submissions.json", "r") as f:
|
|
20
|
+
submissions = json.load(f)
|
|
21
|
+
if additional_functions is None:
|
|
22
|
+
additional_functions = {}
|
|
23
|
+
self.candidates = functions if functions is not None else CodingGame.clean(submissions)
|
|
24
|
+
self.candidates = {**self.candidates, **{x: generate_function(y) if isinstance(y, str) else y for (x, y) in additional_functions.items()}}
|
|
25
|
+
self.plot_frequency = plot_frequency
|
|
26
|
+
self.eliminate = eliminate_slow_teams
|
|
27
|
+
self.length = length_in_seconds
|
|
28
|
+
self.plot_width = plot_width
|
|
29
|
+
self.last_lockout = lockout
|
|
30
|
+
self.max_rounds = rounds
|
|
31
|
+
self.random_draw_function = random_draw_function
|
|
32
|
+
self.must_be_only_team = must_be_only_team
|
|
33
|
+
if isinstance(self.random_draw_function, str):
|
|
34
|
+
if self.random_draw_function.lower().strip() == 'a':
|
|
35
|
+
self.random_draw_function = lambda: np.random.exponential(10)
|
|
36
|
+
elif self.random_draw_function.lower().strip() == 'b':
|
|
37
|
+
self.random_draw_function = lambda: np.random.normal(-0.2, 1)
|
|
38
|
+
self.init_play()
|
|
39
|
+
self.initialize_ui()
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def clean(candidates):
|
|
43
|
+
return {
|
|
44
|
+
outer_key: generate_function(max(inner_dict.items(), key=lambda item: item[0])[1])
|
|
45
|
+
for outer_key, inner_dict in candidates.items()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def initialize_ui(self):
|
|
49
|
+
self.output = widgets.Output()
|
|
50
|
+
self.plot_output = widgets.Output(layout=dict(width='100%'))
|
|
51
|
+
self.game_ui = widgets.HBox(
|
|
52
|
+
[self.output, self.plot_output],
|
|
53
|
+
layout=widgets.Layout(
|
|
54
|
+
align_items='center',
|
|
55
|
+
justify_content='center',
|
|
56
|
+
width='100%'
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
top_spacer = widgets.Box(layout=widgets.Layout(flex='1 1 auto'))
|
|
60
|
+
bottom_spacer = widgets.Box(layout=widgets.Layout(flex='1 1 auto'))
|
|
61
|
+
self.vertical_box = widgets.VBox(
|
|
62
|
+
[top_spacer, self.game_ui, bottom_spacer],
|
|
63
|
+
layout=widgets.Layout(
|
|
64
|
+
height='100vh',
|
|
65
|
+
display='flex',
|
|
66
|
+
flex_flow='column',
|
|
67
|
+
align_items='center',
|
|
68
|
+
justify_content='center',
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.update_plot()
|
|
73
|
+
|
|
74
|
+
def update_plot(self):
|
|
75
|
+
fig = go.Figure()
|
|
76
|
+
for team in self.candidates:
|
|
77
|
+
n = self.current_round
|
|
78
|
+
step = int(n / 100 + 1)
|
|
79
|
+
fig.add_trace(go.Scatter(
|
|
80
|
+
x=list(range(self.current_round + 1))[::step],
|
|
81
|
+
y=self.team_scores[team][::step],
|
|
82
|
+
mode='lines',
|
|
83
|
+
name=team,
|
|
84
|
+
))
|
|
85
|
+
fig.update_layout(showlegend=True, xaxis_title='Round', yaxis_title='Score', xaxis_range=[0, self.max_rounds], width=self.plot_width)
|
|
86
|
+
with self.plot_output:
|
|
87
|
+
clear_output(wait=True)
|
|
88
|
+
display(fig)
|
|
89
|
+
|
|
90
|
+
def start_new_round(self):
|
|
91
|
+
self.current_round += 1
|
|
92
|
+
self.last_number = self.random_draw_function()
|
|
93
|
+
|
|
94
|
+
def loop(self):
|
|
95
|
+
with self.output:
|
|
96
|
+
while self.current_round < self.max_rounds:
|
|
97
|
+
self.start_new_round()
|
|
98
|
+
eliminate = []
|
|
99
|
+
last_team_to_score = None
|
|
100
|
+
for team, btn in self.candidates.items():
|
|
101
|
+
args = (self.last_number, self.last_lockout, self.current_round, self.max_rounds, self.team_scores[team][-1], [y[-1] for (x, y) in self.team_scores.items() if x != team])
|
|
102
|
+
tic_team = timeit.default_timer()
|
|
103
|
+
took_action = btn(*args) and self.current_round > self.team_blocked_until[team]
|
|
104
|
+
toc_team = timeit.default_timer()
|
|
105
|
+
if self.eliminate and toc_team - tic_team > self.length / self.max_rounds / len(self.candidates):
|
|
106
|
+
eliminate.append(team)
|
|
107
|
+
score = self.last_number if took_action else 0
|
|
108
|
+
self.team_scores[team].append(self.team_scores[team][-1] + score)
|
|
109
|
+
if self.must_be_only_team and took_action and last_team_to_score is not None:
|
|
110
|
+
self.team_scores[team][-1] = self.team_scores[team][-2]
|
|
111
|
+
self.team_scores[last_team_to_score][-1] = self.team_scores[last_team_to_score][-2]
|
|
112
|
+
if took_action:
|
|
113
|
+
self.team_blocked_until[team] = self.current_round + self.last_lockout
|
|
114
|
+
last_team_to_score = team
|
|
115
|
+
for x in eliminate:
|
|
116
|
+
del self.candidates[x]
|
|
117
|
+
toc = timeit.default_timer()
|
|
118
|
+
target = self.start + self.current_round / self.max_rounds * self.length
|
|
119
|
+
if toc < target:
|
|
120
|
+
time.sleep(target - toc)
|
|
121
|
+
if toc > self.last_update + self.plot_frequency:
|
|
122
|
+
self.last_update = toc
|
|
123
|
+
self.update_plot()
|
|
124
|
+
|
|
125
|
+
self.update_plot()
|
|
126
|
+
df = (
|
|
127
|
+
pd.DataFrame(
|
|
128
|
+
{x: self.team_scores[x][-1] for x in self.team_scores}.items(),
|
|
129
|
+
columns=['Name', 'Score']
|
|
130
|
+
)
|
|
131
|
+
.sort_values('Score', ascending=False)
|
|
132
|
+
.reset_index(drop=True)
|
|
133
|
+
.rename(index=lambda x: x + 1)
|
|
134
|
+
.round(2)
|
|
135
|
+
)
|
|
136
|
+
display(df)
|
|
137
|
+
|
|
138
|
+
def init_play(self):
|
|
139
|
+
if self.random_draw_function is None:
|
|
140
|
+
random = np.random.default_rng(seed=None)
|
|
141
|
+
self.random_draw_function= lambda: random.pareto(3)
|
|
142
|
+
self.last_update = -2 ** 31
|
|
143
|
+
self.team_scores = {team: [0] for team in self.candidates}
|
|
144
|
+
self.team_blocked_until = {team: -1 for team in self.candidates}
|
|
145
|
+
self.last_number = None
|
|
146
|
+
self.current_round = 0
|
|
147
|
+
|
|
148
|
+
def play(self, countdown=0):
|
|
149
|
+
|
|
150
|
+
with self.output:
|
|
151
|
+
clear_output()
|
|
152
|
+
with self.plot_output:
|
|
153
|
+
clear_output()
|
|
154
|
+
|
|
155
|
+
display(self.vertical_box)
|
|
156
|
+
with self.output:
|
|
157
|
+
for i in range(countdown, 0, -1):
|
|
158
|
+
print(i)
|
|
159
|
+
time.sleep(1)
|
|
160
|
+
clear_output()
|
|
161
|
+
self.start = timeit.default_timer()
|
|
162
|
+
self.loop()
|
|
163
|
+
|
|
164
|
+
def _ipython_display_(self):
|
|
165
|
+
display(self.vertical_box)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from flask import Flask, request, jsonify
|
|
3
|
+
from functools import wraps
|
|
4
|
+
import json
|
|
5
|
+
import base64
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
from .blackbox import generate_function
|
|
10
|
+
from .util import get_url
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GameServer:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.name = str(uuid.uuid4())
|
|
16
|
+
self.submission_lock = threading.Lock()
|
|
17
|
+
self.USERNAME = "colabuser"
|
|
18
|
+
self.PASSWORD = "secretcolab"
|
|
19
|
+
|
|
20
|
+
self.submissions = defaultdict(lambda: defaultdict(dict))
|
|
21
|
+
try:
|
|
22
|
+
with open("submissions.json", 'r') as f:
|
|
23
|
+
submissions_loaded = json.load(f)
|
|
24
|
+
for team, team_submissions in submissions_loaded.items():
|
|
25
|
+
for time, team_submission in team_submissions.items():
|
|
26
|
+
self.submissions[team][time] = team_submission
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
pass
|
|
29
|
+
else:
|
|
30
|
+
print("Loaded existing submissions:")
|
|
31
|
+
print(json.dumps(self.submissions, indent=4))
|
|
32
|
+
|
|
33
|
+
self.app = Flask(self.name)
|
|
34
|
+
|
|
35
|
+
def require_auth(f):
|
|
36
|
+
@wraps(f)
|
|
37
|
+
def decorated(*args, **kwargs):
|
|
38
|
+
auth = request.headers.get("Authorization")
|
|
39
|
+
error = None
|
|
40
|
+
if auth and auth.startswith("Basic "):
|
|
41
|
+
try:
|
|
42
|
+
decoded = base64.b64decode(auth.split(" ")[1]).decode("utf-8")
|
|
43
|
+
user, pw = decoded.split(":")
|
|
44
|
+
if user == self.USERNAME and pw == self.PASSWORD:
|
|
45
|
+
return f(*args, **kwargs)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(e)
|
|
48
|
+
error = e
|
|
49
|
+
return jsonify({"error": "Unauthorized"}), 401 if error is None else jsonify({'error': str(error)}), 500
|
|
50
|
+
return decorated
|
|
51
|
+
|
|
52
|
+
@self.app.route("/receive", methods=["POST"])
|
|
53
|
+
@require_auth
|
|
54
|
+
def receive():
|
|
55
|
+
data = request.get_json()
|
|
56
|
+
print("Submission received:", data)
|
|
57
|
+
generate_function(data['submission'])
|
|
58
|
+
with self.submission_lock:
|
|
59
|
+
self.submissions[data['team']][data['time']] = data['submission']
|
|
60
|
+
with open("submissions.json", "w") as f:
|
|
61
|
+
json.dump(dict(self.submissions), f, indent=4)
|
|
62
|
+
print(json.dumps(
|
|
63
|
+
{
|
|
64
|
+
team_name: max(team_submissions.items(), key=lambda x: x[0])[0]
|
|
65
|
+
for (team_name, team_submissions) in self.submissions.items()
|
|
66
|
+
},
|
|
67
|
+
indent=4
|
|
68
|
+
))
|
|
69
|
+
return jsonify({"status": "success"})
|
|
70
|
+
|
|
71
|
+
def run(self, force_restart=False):
|
|
72
|
+
server_id = get_url(force_restart)
|
|
73
|
+
print('Serving', server_id)
|
|
74
|
+
self.app.run(port=5000)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import subprocess
|
|
3
|
+
import requests
|
|
4
|
+
from pyngrok import ngrok, conf
|
|
5
|
+
|
|
6
|
+
NGROK_API = "http://localhost:4040/api/tunnels"
|
|
7
|
+
|
|
8
|
+
def _cleanup_local_tunnels():
|
|
9
|
+
try:
|
|
10
|
+
r = requests.get(NGROK_API, timeout=1)
|
|
11
|
+
for t in r.json().get("tunnels", []):
|
|
12
|
+
old_url = t["public_url"]
|
|
13
|
+
print(f"NOTE: disconnecting and cleaning up {old_url} before serving new session.")
|
|
14
|
+
ngrok.disconnect(old_url)
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def _ensure_ngrok_running():
|
|
19
|
+
try:
|
|
20
|
+
requests.get(NGROK_API, timeout=1)
|
|
21
|
+
except Exception:
|
|
22
|
+
ngrok.install_ngrok()
|
|
23
|
+
subprocess.Popen(
|
|
24
|
+
[conf.get_default().ngrok_path, "http", "5000"],
|
|
25
|
+
stdout=subprocess.DEVNULL,
|
|
26
|
+
stderr=subprocess.DEVNULL,
|
|
27
|
+
start_new_session=True,
|
|
28
|
+
)
|
|
29
|
+
time.sleep(5)
|
|
30
|
+
|
|
31
|
+
def get_url(force_restart=False):
|
|
32
|
+
if force_restart:
|
|
33
|
+
_cleanup_local_tunnels()
|
|
34
|
+
|
|
35
|
+
_ensure_ngrok_running()
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
r = requests.get(NGROK_API)
|
|
39
|
+
tunnels = r.json()["tunnels"]
|
|
40
|
+
public_url = tunnels[0]["public_url"]
|
|
41
|
+
return public_url.split('//')[1].split('.ngrok-free.app')[0]
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise RuntimeError("ngrok tunnel not available") from e
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import ipywidgets as widgets
|
|
3
|
+
from IPython.display import display, clear_output
|
|
4
|
+
import plotly.graph_objs as go
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ManualGame:
|
|
9
|
+
def __init__(self, team_names, max_rounds=30, lockout=3, width=1000, random_draw_function='a'):
|
|
10
|
+
self.team_names = team_names
|
|
11
|
+
self.max_rounds = max_rounds
|
|
12
|
+
self.lockout = lockout
|
|
13
|
+
self.random_draw_function = random_draw_function
|
|
14
|
+
if isinstance(self.random_draw_function, str):
|
|
15
|
+
if self.random_draw_function.lower().strip() == 'a':
|
|
16
|
+
self.random_draw_function = lambda: numpy.random.exponential(10)
|
|
17
|
+
elif self.random_draw_function.lower().strip() == 'b':
|
|
18
|
+
self.random_draw_function = lambda: numpy.random.normal(-0.2, 1)
|
|
19
|
+
self.width = width
|
|
20
|
+
self.initialize_ui()
|
|
21
|
+
|
|
22
|
+
def initialize_ui(self):
|
|
23
|
+
self.current_round = 0
|
|
24
|
+
self.team_scores = {team: [0] for team in self.team_names}
|
|
25
|
+
self.team_blocked_until = {team: -1 for team in self.team_names}
|
|
26
|
+
self.last_number = None
|
|
27
|
+
self.last_lockout = self.lockout
|
|
28
|
+
self.number_display = widgets.HTML(layout=dict(width='80%'))
|
|
29
|
+
self.team_buttons = {
|
|
30
|
+
team: widgets.ToggleButton(description=team, value=False, tooltip=team, layout=dict(width='50%'))
|
|
31
|
+
for team in self.team_names
|
|
32
|
+
}
|
|
33
|
+
self.submit_button = widgets.Button(description="Submit Selections", button_style='success', layout=dict(width='50%'))
|
|
34
|
+
self.output = widgets.Output()
|
|
35
|
+
self.controls = widgets.VBox([
|
|
36
|
+
self.number_display,
|
|
37
|
+
*self.team_buttons.values(),
|
|
38
|
+
self.submit_button,
|
|
39
|
+
self.output,
|
|
40
|
+
],
|
|
41
|
+
layout=dict(width='40%', align_items='center')
|
|
42
|
+
)
|
|
43
|
+
self.plot_output = widgets.Output(layout=dict(width='100%'))
|
|
44
|
+
|
|
45
|
+
def update_plot():
|
|
46
|
+
fig = go.Figure()
|
|
47
|
+
for team in self.team_names:
|
|
48
|
+
fig.add_trace(go.Scatter(
|
|
49
|
+
x=list(range(self.current_round + 1)),
|
|
50
|
+
y=self.team_scores[team],
|
|
51
|
+
mode='lines+markers',
|
|
52
|
+
name=team
|
|
53
|
+
))
|
|
54
|
+
fig.update_layout(xaxis_title='Round', yaxis_title='Score', xaxis_range=[0, self.max_rounds], width=self.width)
|
|
55
|
+
with self.plot_output:
|
|
56
|
+
clear_output(wait=True)
|
|
57
|
+
display(fig)
|
|
58
|
+
|
|
59
|
+
def start_new_round():
|
|
60
|
+
self.current_round += 1
|
|
61
|
+
self.last_number = self.random_draw_function()
|
|
62
|
+
|
|
63
|
+
self.number_display.value = f"""
|
|
64
|
+
<div style='text-align:center; padding: 10px;'>
|
|
65
|
+
<h1 style='margin-bottom: 0;'>Round {self.current_round} / {self.max_rounds} <br> Lockout: {self.last_lockout}</h1>
|
|
66
|
+
<h2 style='color:darkred; font-size: 48px; margin-top: 10px;'>Reward: {self.last_number:.2f}</h2>
|
|
67
|
+
</div>
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
for team, btn in self.team_buttons.items():
|
|
71
|
+
blocked = self.team_blocked_until[team] - self.current_round
|
|
72
|
+
is_blocked = blocked >= 0
|
|
73
|
+
btn.disabled = is_blocked
|
|
74
|
+
btn.value = False
|
|
75
|
+
btn.description = f'❌ {team} ({blocked + 1})' if is_blocked else team
|
|
76
|
+
|
|
77
|
+
def on_submit(_):
|
|
78
|
+
with self.output:
|
|
79
|
+
clear_output()
|
|
80
|
+
for team, btn in self.team_buttons.items():
|
|
81
|
+
took_action = btn.value and not btn.disabled
|
|
82
|
+
score = self.last_number if took_action else 0
|
|
83
|
+
self.team_scores[team].append(self.team_scores[team][-1] + score)
|
|
84
|
+
if took_action:
|
|
85
|
+
self.team_blocked_until[team] = self.current_round + self.last_lockout
|
|
86
|
+
update_plot()
|
|
87
|
+
if self.current_round < self.max_rounds:
|
|
88
|
+
start_new_round()
|
|
89
|
+
else:
|
|
90
|
+
df = (
|
|
91
|
+
pd.DataFrame(
|
|
92
|
+
{x: self.team_scores[x][-1] for x in self.team_scores}.items(),
|
|
93
|
+
columns=['Name', 'Score']
|
|
94
|
+
)
|
|
95
|
+
.sort_values('Score', ascending=False)
|
|
96
|
+
.reset_index(drop=True)
|
|
97
|
+
.rename(index=lambda x: x + 1)
|
|
98
|
+
.round(2)
|
|
99
|
+
)
|
|
100
|
+
self.number_display.value = ''
|
|
101
|
+
self.submit_button.layout.display = 'none'
|
|
102
|
+
for x in self.team_buttons.values():
|
|
103
|
+
x.layout.display = 'none'
|
|
104
|
+
with self.output:
|
|
105
|
+
display(df)
|
|
106
|
+
|
|
107
|
+
self.submit_button.on_click(on_submit)
|
|
108
|
+
self.game_ui = widgets.HBox(
|
|
109
|
+
[self.controls, self.plot_output],
|
|
110
|
+
layout=widgets.Layout(
|
|
111
|
+
align_items='center',
|
|
112
|
+
justify_content='center',
|
|
113
|
+
border='solid 1px gray',
|
|
114
|
+
width='100%'
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
top_spacer = widgets.Box(layout=widgets.Layout(flex='1 1 auto'))
|
|
119
|
+
bottom_spacer = widgets.Box(layout=widgets.Layout(flex='1 1 auto'))
|
|
120
|
+
self.vertical_box = widgets.VBox(
|
|
121
|
+
[top_spacer, self.game_ui, bottom_spacer],
|
|
122
|
+
layout=widgets.Layout(
|
|
123
|
+
height='100vh',
|
|
124
|
+
display='flex',
|
|
125
|
+
flex_flow='column',
|
|
126
|
+
align_items='center',
|
|
127
|
+
justify_content='center',
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
update_plot()
|
|
132
|
+
start_new_round()
|
|
133
|
+
|
|
134
|
+
def _ipython_display_(self):
|
|
135
|
+
display(self.vertical_box)
|
|
136
|
+
|
gr_tradinggame/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gr_tradinggame
|
|
3
|
+
Version: 0.1.13
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Soeren Wolfers <soeren.wolfers@gresearch.com>, Ajay Mittal <ajaymittal2001@gmail.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: cloudpickle==3.1.1
|
|
8
|
+
Requires-Dist: flask-ngrok==0.0.25
|
|
9
|
+
Requires-Dist: flask==3.1.1
|
|
10
|
+
Requires-Dist: ipython==7.34.0
|
|
11
|
+
Requires-Dist: ipywidgets==7.7.1
|
|
12
|
+
Requires-Dist: numpy>=2.0.2
|
|
13
|
+
Requires-Dist: pandas>=2.2.2
|
|
14
|
+
Requires-Dist: plotly==5.10.0
|
|
15
|
+
Requires-Dist: pyngrok==7.2.8
|
|
16
|
+
Requires-Dist: requests>=2.32.3
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Trading game for recruitment events.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
`pip install gr-tradinggame`
|
|
24
|
+
|
|
25
|
+
### Manual game
|
|
26
|
+
|
|
27
|
+
1. Run the code in `notebooks/example_manual.ipynb` in a notebook (looks best on Colab, with fullscreen cell display). [](https://colab.research.google.com/github/soerenwolfers/gr_tradinggame/blob/main/notebooks/example_manual.ipynb)
|
|
28
|
+
|
|
29
|
+
2. Have teams shout / show their decisions and enter them by clicking buttons.
|
|
30
|
+
|
|
31
|
+
### Coding game
|
|
32
|
+
|
|
33
|
+
1. Run the code in `notebooks/example_server.ipynb`. [](https://colab.research.google.com/github/soerenwolfers/gr_tradinggame/blob/main/notebooks/example_server.ipynb)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
2. The output will contain `Serving <id>`. Give that `id` to the teams and have them test and submit their solutions with that `id` as in `notebooks/example_client.ipynb`. [](https://colab.research.google.com/github/soerenwolfers/gr_tradinggame/blob/main/notebooks/example_client.ipynb)
|
|
38
|
+
|
|
39
|
+
3. Run the remaining cells in your server notebook to see who wins.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
gr_tradinggame/__init__.py,sha256=f_XkRXIp0URwPWFmsMMKwN12zySh3mCtr55hiM3ZrYI,59
|
|
2
|
+
gr_tradinggame/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
gr_tradinggame/coding/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
gr_tradinggame/coding/blackbox.py,sha256=tOcgtQB9lgT6ZG0S_IT8wszZlLL6gZx4UT8TBk5g5r0,1959
|
|
5
|
+
gr_tradinggame/coding/client.py,sha256=h1QlRC1jmFim-IdCclMA9fJa3i-mG7LA7AdEIUd1BvI,5427
|
|
6
|
+
gr_tradinggame/coding/gui.py,sha256=lXgMj3qz5BNVhpYapPLAsHWBimoidgrP5S_LxDnsKsg,6841
|
|
7
|
+
gr_tradinggame/coding/server.py,sha256=s3Z4QaPAC6-w4rLhZFHaASJzp-SvjxB3HjRm164PTOs,2791
|
|
8
|
+
gr_tradinggame/coding/util.py,sha256=Z4lGVhhMOPGZJwjZ3Wu5a8LAfG0vIyHd7KEFaOE6b2o,1243
|
|
9
|
+
gr_tradinggame/manual/__init__.py,sha256=ml818maV8iA2J9S8VWrIGyiKRbRFuGcFoXB7LIkME2A,5551
|
|
10
|
+
gr_tradinggame-0.1.13.dist-info/METADATA,sha256=Vm-9lmOyI8aycdH9wINx4gWGU-dXqY4Sl700VYrdtiA,1760
|
|
11
|
+
gr_tradinggame-0.1.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
gr_tradinggame-0.1.13.dist-info/RECORD,,
|