-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhilo.py
489 lines (402 loc) · 17.3 KB
/
hilo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
from iconservice import *
TAG = "HILO"
DEBUG = True
MAIN_BET_MULTIPLIER = 98.5
BET_MIN = 100000000000000000
CARD_SUITES = [
'', # 0
'HEART', # 1
'DIAMOND', # 2
'SPADE', # 3
'CLUB' # 4
]
CARD_SUITE_COLORS = [
'', # 0
'RED', # 1
'RED', # 2
'BLACK', # 3
'BLACK' # 4
]
SIDE_BET_TYPES = {
1: "COLOR_RED",
2: "COLOR_BLACK",
3: "GROUP_FIRST", # 2-3-4-5
4: "GROUP_SECOND", # 6-7-8-9
5: "GROUP_THIRD", # J-Q-K-A
}
SIDE_BET_MULTIPLIERS = {
1: 1.961,
2: 1.961,
3: 2.941,
4: 2.941,
5: 2.941
}
BET_LIMIT_RATIOS_SIDE_BET = {
1: 2000,
2: 2000,
3: 500,
4: 500,
5: 500
}
CARD_TITLES = {
1: '2',
2: '3',
3: '4',
4: '5',
5: '6',
6: '7',
7: '8',
8: '9',
9: 'J',
10: 'Q',
11: 'K',
12: 'A'
}
# An interface to treasury score
class TreasuryInterface(InterfaceScore):
@interface
def get_treasury_min(self) -> int:
pass
@interface
def send_wager(self, _amount: int) -> None:
pass
@interface
def wager_payout(self, _payout: int) -> None:
pass
class HiLo(IconScoreBase):
_GAME_ON = "game_on"
_TREASURY_SCORE = "treasury_score"
_USER_CARD = "user_card"
_ADMIN_ADDRESS = "Admin_Address"
def __init__(self, db: IconScoreDatabase) -> None:
super().__init__(db)
if DEBUG is True:
Logger.debug(f'In __init__.', TAG)
Logger.debug(f'owner is {self.owner}.', TAG)
self._game_on = VarDB(self._GAME_ON, db, value_type=bool)
self._game_admin = VarDB(self._ADMIN_ADDRESS, db, value_type=Address)
self._treasury_score = VarDB(self._TREASURY_SCORE, db, value_type=Address)
self._user_card = DictDB(self._USER_CARD, db, value_type=int)
@eventlog(indexed=3)
def BetPlaced(self, amount: int, bet_type: int, prev_card: int):
pass
@eventlog(indexed=2)
def OldCard(self, card_number: str, card_suite: str):
pass
@eventlog(indexed=2)
def NewCard(self, card_number: str, card_suite: str):
pass
@eventlog(indexed=2)
def BetSource(self, _from: Address, timestamp: int):
pass
@eventlog(indexed=3)
def PayoutAmount(self, payout: int, main_bet_payout: int, side_bet_payout: int):
pass
@eventlog(indexed=3)
def BetResult(self, new_card: int, old_card: int, payout: int):
pass
@eventlog(indexed=3)
def DebugPayout(self, payout: int, main_bet_payout: int, side_bet_payout: int):
pass
@eventlog(indexed=2)
def FundTransfer(self, recipient: Address, amount: int, note: str):
pass
def on_install(self) -> None:
super().on_install()
self._game_on.set(False)
def on_update(self) -> None:
super().on_update()
@external(readonly=True)
def get_score_owner(self) -> Address:
"""
A function to return the owner of this score.
:return: Owner address of this score
:rtype: :class:`iconservice.base.address.Address`
"""
return self.owner
@external
def set_treasury_score(self, _score: Address) -> None:
"""
Sets the treasury score address. The function can only be invoked by score owner.
:param _score: Score address of the treasury
:type _score: :class:`iconservice.base.address.Address`
"""
if self.msg.sender == self.owner:
self._treasury_score.set(_score)
@external(readonly=True)
def get_treasury_score(self) -> Address:
"""
Returns the treasury score address.
:return: Address of the treasury score
:rtype: :class:`iconservice.base.address.Address`
"""
return self._treasury_score.get()
@external
def set_game_admin(self, admin_address: Address) -> None:
if self.msg.sender != self.owner:
revert('Only the owner can call set_game_admin method')
self._game_admin.set(admin_address)
@external(readonly=True)
def get_game_admin(self) -> Address:
"""
A function to return the admin of the game
:return: Address
"""
return self._game_admin.get()
@external
def game_on(self) -> None:
"""
Set the status of game as on. Only the owner of the game can call this method.
Owner must have set the treasury score before changing the game status as on.
"""
if self.msg.sender != self.owner:
revert('Only the owner can call the game_on method')
if not self._game_on.get() and self._treasury_score.get() is not None:
self._game_on.set(True)
@external
def game_off(self) -> None:
"""
Set the status of game as off. Only the owner of the game can call this method.
"""
if self.msg.sender != self.owner:
revert('Only the owner can call the game_on method')
if self._game_on.get():
self._game_on.set(False)
@external(readonly=True)
def get_game_on(self) -> bool:
"""
Returns the current game status
:return: Current game status
:rtype: bool
"""
return self._game_on.get()
@external
def clear_user(self) -> bool:
user_id = self.tx.origin
self._user_card[user_id] = 0
return True
@external
def untether(self) -> None:
"""
A function to redefine the value of self.owner once it is possible .
To be included through an update if it is added to ICONSERVICE
Sets the value of self.owner to the score holding the game treasury
"""
if self.msg.sender != self.owner:
revert('Only the owner can call the untether method ')
pass
@external
def first_call(self, user_seed: str = '') -> int:
self.BetSource(self.tx.origin, self.tx.timestamp)
if not self._game_on.get():
Logger.debug(f'Game not active yet.', TAG)
revert(f'Game not active yet.')
user_id = self.tx.origin
if self._user_card[user_id] != 0:
cardNumber, cardSuite = self.get_real_card(self._user_card[user_id])
self.NewCard(CARD_TITLES[cardNumber], CARD_SUITES[cardSuite])
return self._user_card[user_id]
cardNumber, cardSuite = self.get_random_card(user_seed)
self._user_card[user_id] = self.get_normalized_card(cardNumber, cardSuite)
self.NewCard(CARD_TITLES[cardNumber], CARD_SUITES[cardSuite])
return self._user_card[user_id]
@payable
@external
def call_bet(self, main_bet_type: int, user_seed: str = '', side_bet_amount: int = 0,
side_bet_type: int = 0) -> None:
"""
Main bet function. It takes the upper and lower number for bet. Upper and lower number must be in the range
[0,99]. The difference between upper and lower can be in the range of [0,95].
Bet types:
0 - none
1 - lower
2 - upper
3 - match
4 - unmatch
:param main_bet_type: User bet type
:type upper: int
:param user_seed: 'Lucky phrase' provided by user, defaults to ""
:type user_seed: str,optional
"""
return self.__bet(main_bet_type, user_seed, side_bet_amount, side_bet_type)
def __bet(self, main_bet_type: int, user_seed :str, side_bet_amount: int, side_bet_type: int) -> None:
side_bet_won = False
side_bet_set = False
side_bet_payout = 0
self.BetSource(self.tx.origin, self.tx.timestamp)
treasury_score = self.create_interface_score(self._treasury_score.get(), TreasuryInterface)
_treasury_min = treasury_score.get_treasury_min()
self.icx.transfer(self._treasury_score.get(), self.msg.value)
self.FundTransfer(self._treasury_score.get(), self.msg.value, "Sending icx to Treasury")
treasury_score.icx(self.msg.value).send_wager(self.msg.value)
# Guards
if not self._game_on.get():
Logger.debug(f'Game not active yet.', TAG)
revert(f'Game not active yet.')
if main_bet_type == 0 and side_bet_type == 0:
Logger.debug(f'Need at least one bet(main/side)', TAG)
revert(f'Need at least one bet(main/side)')
if not (0 <= main_bet_type <= 4):
Logger.debug(f'Invalid bet type', TAG)
revert(f'Invalid bet type')
if main_bet_type == 4 and side_bet_type != 0:
Logger.debug(f'Can not play unmatch with side bet!', TAG)
revert(f'Can not play unmatch with side bet!')
if (side_bet_type == 0 and side_bet_amount != 0) or (side_bet_type != 0 and side_bet_amount == 0):
Logger.debug(f'should set both side bet type as well as side bet amount', TAG)
revert(f'should set both side bet type as well as side bet amount')
if side_bet_amount < 0:
revert(f'Side bet amount cannot be negative')
# unmatch is always played with red/black, it is not considered as sidebet!
if main_bet_type != 4 and side_bet_type != 0 and side_bet_amount != 0:
side_bet_set = True
user_id = self.tx.origin
main_bet_amount = self.msg.value - side_bet_amount
user_prev_card = self._user_card[user_id]
if user_prev_card == 0:
# previos card does not exist
Logger.debug(f'Start game for user first!', TAG)
revert(f'Start game for user first!')
oldCardNumber, oldCardSuite = self.get_real_card(user_prev_card)
self.BetPlaced(main_bet_amount, main_bet_type, user_prev_card)
if (main_bet_type == 1 and oldCardNumber == 1) or (main_bet_type == 2 and oldCardNumber == 12):
Logger.debug(f'Invalid main bet!', TAG)
revert(f'Invalid main bet!')
if main_bet_type != 0: # If main bet is played
main_bet_limit = self.calculate_bet_limit(main_bet_type, oldCardNumber, _treasury_min)
if main_bet_amount < BET_MIN or main_bet_amount > main_bet_limit:
Logger.debug(f'Betting amount {main_bet_amount} out of range.', TAG)
revert(f'Bet amount {main_bet_amount} out of range {BET_MIN},{main_bet_limit} ')
main_bet_payout = 0
if main_bet_type != 0: # If main bet is played
gap = self.calculate_gap(main_bet_type, oldCardNumber)
main_bet_payout = self.calculate_bet_payout(gap, main_bet_amount)
if self.icx.get_balance(self._treasury_score.get()) < main_bet_payout + side_bet_payout:
Logger.debug(f'Not enough in treasury to make the play.', TAG)
revert('Not enough in treasury to make the play.')
# Actual bet part
cardNumber, cardSuite = self.get_random_card(user_seed)
main_bet_won = False
if cardNumber < oldCardNumber and main_bet_type == 1:
main_bet_won = True
elif cardNumber > oldCardNumber and main_bet_type == 2:
main_bet_won = True
elif cardNumber == oldCardNumber and main_bet_type == 3:
main_bet_won = True
elif main_bet_type == 4:
if cardNumber != oldCardNumber:
main_bet_won = True
elif CARD_SUITE_COLORS[cardSuite] != CARD_SUITE_COLORS[oldCardSuite]:
main_bet_won = True
if side_bet_set:
side_bet_won = self.check_side_bet_win(side_bet_type, cardNumber, cardSuite)
if not side_bet_won:
side_bet_payout = 0
else:
if side_bet_type not in SIDE_BET_TYPES:
Logger.debug(f'Invalid side bet type', TAG)
revert(f'Invalid side bet type.')
side_bet_limit = _treasury_min // BET_LIMIT_RATIOS_SIDE_BET[side_bet_type]
if side_bet_amount < BET_MIN or side_bet_amount > side_bet_limit:
Logger.debug(f'Betting amount {side_bet_amount} out of range.', TAG)
revert(f'Betting amount {side_bet_amount} out of range ({BET_MIN} ,{side_bet_limit}).')
side_bet_payout = int(SIDE_BET_MULTIPLIERS[side_bet_type] * 100) * side_bet_amount // 100
normalizedNewCard = self.get_normalized_card(cardNumber, cardSuite)
Logger.debug(f'Old card: {user_prev_card} new card {normalizedNewCard} has won {main_bet_won} bet amount {main_bet_amount}', TAG)
self.DebugPayout(main_bet_payout * main_bet_won + side_bet_payout, main_bet_payout, side_bet_payout)
main_bet_payout = main_bet_payout * main_bet_won
payout = main_bet_payout + side_bet_payout
self._user_card[user_id] = normalizedNewCard
self.BetResult(normalizedNewCard, user_prev_card, payout)
self.PayoutAmount(payout, main_bet_payout, side_bet_payout)
self.OldCard(CARD_TITLES[oldCardNumber], CARD_SUITES[oldCardSuite])
self.NewCard(CARD_TITLES[cardNumber], CARD_SUITES[cardSuite])
if main_bet_won or side_bet_won:
Logger.debug(f'Amount owed to winner: {payout}', TAG)
try:
Logger.debug(f'Trying to send to ({self.tx.origin}): {payout}.', TAG)
_treasury_score.wager_payout(payout)
Logger.debug(f'Sent winner ({self.tx.origin}) {payout}.', TAG)
except BaseException as e:
Logger.debug(f'Send failed. Exception: {e}', TAG)
revert('Network problem. Winnings not sent. Returning funds.')
else:
Logger.debug(f'Player lost. ICX retained in treasury.', TAG)
def get_random(self, user_seed: str = '') -> float:
"""
Generates a random # from tx hash, block timestamp and user provided
seed. The block timestamp provides the source of unpredictability.
:param user_seed: 'Lucky phrase' provided by user, defaults to ""
:type user_seed: str,optional
:return: Number from [x / 100000.0 for x in range(100000)]
:rtype: float
"""
Logger.debug(f'Entered get_random.', TAG)
seed = (str(bytes.hex(self.tx.hash)) + str(self.now()) + user_seed)
spin = (int.from_bytes(sha3_256(seed.encode()), "big") % 100000) / 100000.0
Logger.debug(f'Result of the spin was {spin}.', TAG)
return spin
def get_random_card(self, user_seed: str = '') -> [int, int]:
spin = self.get_random(user_seed)
return self.get_real_card(int(spin * 48) + 1)
def get_normalized_card(self, cardNumber: int, cardSuite: int) -> int:
return (cardSuite - 1) * 12 + cardNumber
def get_real_card(self, cardAsInt: int = 0) -> [int, int]:
cardMod = cardAsInt % 12
cardNumber = 12 if cardMod == 0 else cardMod
cardSuite = self.ceil(cardAsInt / 12)
return cardNumber, cardSuite
def ceil(self, number: float) -> int:
return int(number) + int((number > 0) and (number - int(number)) > 0)
@external(readonly=True)
def current_card(self, user_id: Address) -> list:
# user has no card
if self._user_card[user_id] == 0:
return ['-1', '-1']
cardNumber, cardSuite = self.get_real_card(self._user_card[user_id])
return [CARD_TITLES[cardNumber], CARD_SUITES[cardSuite]]
def calculate_bet_limit(self, bet_type: int, user_prev_card_number: int, treasury_min: int) -> int:
gap = self.calculate_gap(bet_type, user_prev_card_number)
return int((treasury_min * 1.5 * gap) // (68134 - 681.34 * gap))
def calculate_bet_payout(self, gap: int, bet_amount: int) -> int:
return int(int(MAIN_BET_MULTIPLIER * 100) * bet_amount // (100 * gap))
def calculate_gap(self, bet_type: int, user_prev_card_number: int) -> float:
if bet_type == 0: # main bet not played, this should not even be called
gap = 0
elif bet_type == 1: # lower
gap = user_prev_card_number - 1
elif bet_type == 2: # upper
gap = 12 - user_prev_card_number
elif bet_type == 3: # match win rate 1/12
gap = 1
elif bet_type == 4: # unmatch win rate 23/24
gap = 11.5
# scale from 12 to 100.
return gap * 100 / 12
# check for bet limits and side limits
def check_side_bet_win(self, side_bet_type: int, cardNumber: int, cardSuite: str) -> bool:
"""
Checks the conditions for side bets are matched or not.
:param side_bet_type: side bet types can be one of this ["digits_match", "icon_logo1","icon_logo2"], defaults to
""
:type side_bet_type: str,optional
:param winning_number: winning number returned by random function
:type winning_number: int
:return: Returns true or false based on the side bet type and the winning number
:rtype: bool
"""
if side_bet_type == 1: # red
return cardSuite in [1, 2]
elif side_bet_type == 2: # black
return cardSuite in [3, 4]
elif side_bet_type == 3: # group 1-4 (2, 3, 4, 5)
return cardNumber in [1, 2, 3, 4]
elif side_bet_type == 4: # group 5-8 (6, 7, 8, 9)
return cardNumber in [5, 6, 7, 8]
elif side_bet_type == 5: # group 9-12 (J, Q, K, A)
return cardNumber in [9, 10, 11, 12]
else:
return False
@payable
def fallback(self):
pass