bauiv1lib.tournamententry
Defines a popup window for entering tournaments.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines a popup window for entering tournaments.""" 4 5from __future__ import annotations 6 7import logging 8from typing import TYPE_CHECKING, override 9 10from bauiv1lib.popup import PopupWindow 11import bauiv1 as bui 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 import bascenev1 as bs 16 17 18class TournamentEntryWindow(PopupWindow): 19 """Popup window for entering tournaments.""" 20 21 def __init__( 22 self, 23 tournament_id: str, 24 tournament_activity: bs.Activity | None = None, 25 position: tuple[float, float] = (0.0, 0.0), 26 delegate: Any = None, 27 scale: float | None = None, 28 offset: tuple[float, float] = (0.0, 0.0), 29 on_close_call: Callable[[], Any] | None = None, 30 ): 31 # Needs some tidying. 32 # pylint: disable=too-many-locals 33 # pylint: disable=too-many-branches 34 # pylint: disable=too-many-statements 35 36 assert bui.app.classic is not None 37 assert bui.app.plus 38 bui.set_analytics_screen('Tournament Entry Window') 39 40 self._tournament_id = tournament_id 41 self._tournament_info = bui.app.classic.accounts.tournament_info[ 42 self._tournament_id 43 ] 44 45 # Set a few vars depending on the tourney fee. 46 self._fee = self._tournament_info['fee'] 47 self._allow_ads = self._tournament_info['allowAds'] 48 if self._fee == 4: 49 self._purchase_name = 'tournament_entry_4' 50 self._purchase_price_name = 'price.tournament_entry_4' 51 elif self._fee == 3: 52 self._purchase_name = 'tournament_entry_3' 53 self._purchase_price_name = 'price.tournament_entry_3' 54 elif self._fee == 2: 55 self._purchase_name = 'tournament_entry_2' 56 self._purchase_price_name = 'price.tournament_entry_2' 57 elif self._fee == 1: 58 self._purchase_name = 'tournament_entry_1' 59 self._purchase_price_name = 'price.tournament_entry_1' 60 else: 61 if self._fee != 0: 62 raise ValueError('invalid fee: ' + str(self._fee)) 63 self._purchase_name = 'tournament_entry_0' 64 self._purchase_price_name = 'price.tournament_entry_0' 65 66 self._purchase_price: int | None = None 67 68 self._on_close_call = on_close_call 69 if scale is None: 70 uiscale = bui.app.ui_v1.uiscale 71 scale = ( 72 2.3 73 if uiscale is bui.UIScale.SMALL 74 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 75 ) 76 self._delegate = delegate 77 self._transitioning_out = False 78 79 self._tournament_activity = tournament_activity 80 81 self._width: float = 340.0 82 self._height: float = 225.0 83 84 bg_color = (0.5, 0.4, 0.6) 85 86 # Show the practice button as long as we're not 87 # restarting while on a paid tournament run. 88 self._do_practice = ( 89 self._tournament_activity is None 90 and bui.app.config.get('tournament_practice_enabled', False) 91 ) 92 93 off_p = 0 if not self._do_practice else 48 94 self._height += off_p * 0.933 95 96 # Creates our root_widget. 97 super().__init__( 98 position=position, 99 size=(self._width, self._height), 100 scale=scale, 101 bg_color=bg_color, 102 offset=offset, 103 toolbar_visibility='menu_currency', 104 ) 105 106 self._last_ad_press_time = -9999.0 107 self._last_ticket_press_time = -9999.0 108 self._entering = False 109 self._launched = False 110 111 # Show the ad button only if we support ads *and* it has a level 1 fee. 112 self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads 113 114 x_offs = 0 if self._do_ad_btn else 85 115 116 self._cancel_button = bui.buttonwidget( 117 parent=self.root_widget, 118 position=(40, self._height - 34), 119 size=(60, 60), 120 scale=0.5, 121 label='', 122 color=bg_color, 123 on_activate_call=self._on_cancel, 124 autoselect=True, 125 icon=bui.gettexture('crossOut'), 126 iconscale=1.2, 127 ) 128 129 self._title_text = bui.textwidget( 130 parent=self.root_widget, 131 position=(self._width * 0.5, self._height - 20), 132 size=(0, 0), 133 h_align='center', 134 v_align='center', 135 scale=0.6, 136 text=bui.Lstr(resource='tournamentEntryText'), 137 maxwidth=180, 138 color=(1, 1, 1, 0.4), 139 ) 140 141 btn = self._pay_with_tickets_button = bui.buttonwidget( 142 parent=self.root_widget, 143 position=(30 + x_offs, 60 + off_p), 144 autoselect=True, 145 button_type='square', 146 size=(120, 120), 147 label='', 148 on_activate_call=self._on_pay_with_tickets_press, 149 ) 150 self._ticket_img_pos = (50 + x_offs, 94 + off_p) 151 self._ticket_img_pos_free = (50 + x_offs, 80 + off_p) 152 self._ticket_img = bui.imagewidget( 153 parent=self.root_widget, 154 draw_controller=btn, 155 size=(80, 80), 156 position=self._ticket_img_pos, 157 texture=bui.gettexture('tickets'), 158 ) 159 self._ticket_cost_text_position = (87 + x_offs, 88 + off_p) 160 self._ticket_cost_text_position_free = (87 + x_offs, 120 + off_p) 161 self._ticket_cost_text = bui.textwidget( 162 parent=self.root_widget, 163 draw_controller=btn, 164 position=self._ticket_cost_text_position, 165 size=(0, 0), 166 h_align='center', 167 v_align='center', 168 scale=0.6, 169 text='', 170 maxwidth=95, 171 color=(0, 1, 0), 172 ) 173 self._free_plays_remaining_text = bui.textwidget( 174 parent=self.root_widget, 175 draw_controller=btn, 176 position=(87 + x_offs, 78 + off_p), 177 size=(0, 0), 178 h_align='center', 179 v_align='center', 180 scale=0.33, 181 text='', 182 maxwidth=95, 183 color=(0, 0.8, 0), 184 ) 185 self._pay_with_ad_btn: bui.Widget | None 186 if self._do_ad_btn: 187 btn = self._pay_with_ad_btn = bui.buttonwidget( 188 parent=self.root_widget, 189 position=(190, 60 + off_p), 190 autoselect=True, 191 button_type='square', 192 size=(120, 120), 193 label='', 194 on_activate_call=self._on_pay_with_ad_press, 195 ) 196 self._pay_with_ad_img = bui.imagewidget( 197 parent=self.root_widget, 198 draw_controller=btn, 199 size=(80, 80), 200 position=(210, 94 + off_p), 201 texture=bui.gettexture('tv'), 202 ) 203 204 self._ad_text_position = (251, 88 + off_p) 205 self._ad_text_position_remaining = (251, 92 + off_p) 206 have_ad_tries_remaining = ( 207 self._tournament_info['adTriesRemaining'] is not None 208 ) 209 self._ad_text = bui.textwidget( 210 parent=self.root_widget, 211 draw_controller=btn, 212 position=( 213 self._ad_text_position_remaining 214 if have_ad_tries_remaining 215 else self._ad_text_position 216 ), 217 size=(0, 0), 218 h_align='center', 219 v_align='center', 220 scale=0.6, 221 # Note: AdMob now requires rewarded ad usage 222 # specifically says 'Ad' in it. 223 text=bui.Lstr(resource='watchAnAdText'), 224 maxwidth=95, 225 color=(0, 1, 0), 226 ) 227 ad_plays_remaining_text = ( 228 '' 229 if not have_ad_tries_remaining 230 else '' + str(self._tournament_info['adTriesRemaining']) 231 ) 232 self._ad_plays_remaining_text = bui.textwidget( 233 parent=self.root_widget, 234 draw_controller=btn, 235 position=(251, 78 + off_p), 236 size=(0, 0), 237 h_align='center', 238 v_align='center', 239 scale=0.33, 240 text=ad_plays_remaining_text, 241 maxwidth=95, 242 color=(0, 0.8, 0), 243 ) 244 245 bui.textwidget( 246 parent=self.root_widget, 247 position=(self._width * 0.5, 120 + off_p), 248 size=(0, 0), 249 h_align='center', 250 v_align='center', 251 scale=0.6, 252 text=bui.Lstr( 253 resource='orText', subs=[('${A}', ''), ('${B}', '')] 254 ), 255 maxwidth=35, 256 color=(1, 1, 1, 0.5), 257 ) 258 else: 259 self._pay_with_ad_btn = None 260 261 btn_size = (150, 45) 262 btn_pos = (self._width / 2 - btn_size[0] / 2, self._width / 2 - 110) 263 self._practice_button = None 264 if self._do_practice: 265 self._practice_button = bui.buttonwidget( 266 parent=self.root_widget, 267 position=btn_pos, 268 autoselect=True, 269 size=btn_size, 270 label=bui.Lstr(resource='practiceText'), 271 on_activate_call=self._on_practice_press, 272 ) 273 274 self._get_tickets_button: bui.Widget | None = None 275 self._ticket_count_text: bui.Widget | None = None 276 if not bui.app.ui_v1.use_toolbars: 277 if bui.app.classic.allow_ticket_purchases: 278 self._get_tickets_button = bui.buttonwidget( 279 parent=self.root_widget, 280 position=(self._width - 190 + 105, self._height - 34), 281 autoselect=True, 282 scale=0.5, 283 size=(120, 60), 284 textcolor=(0.2, 1, 0.2), 285 label=bui.charstr(bui.SpecialChar.TICKET), 286 color=(0.65, 0.5, 0.8), 287 on_activate_call=self._on_get_tickets_press, 288 ) 289 else: 290 self._ticket_count_text = bui.textwidget( 291 parent=self.root_widget, 292 scale=0.5, 293 position=(self._width - 190 + 125, self._height - 34), 294 color=(0.2, 1, 0.2), 295 h_align='center', 296 v_align='center', 297 ) 298 299 self._seconds_remaining = None 300 301 bui.containerwidget( 302 edit=self.root_widget, cancel_button=self._cancel_button 303 ) 304 305 # Let's also ask the server for info about this tournament 306 # (time remaining, etc) so we can show the user time remaining, 307 # disallow entry if time has run out, etc. 308 # xoffs = 104 if bui.app.ui.use_toolbars else 0 309 self._time_remaining_text = bui.textwidget( 310 parent=self.root_widget, 311 position=(self._width / 2, 28), 312 size=(0, 0), 313 h_align='center', 314 v_align='center', 315 text='-', 316 scale=0.65, 317 maxwidth=100, 318 flatness=1.0, 319 color=(0.7, 0.7, 0.7), 320 ) 321 self._time_remaining_label_text = bui.textwidget( 322 parent=self.root_widget, 323 position=(self._width / 2, 45), 324 size=(0, 0), 325 h_align='center', 326 v_align='center', 327 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 328 scale=0.45, 329 flatness=1.0, 330 maxwidth=100, 331 color=(0.7, 0.7, 0.7), 332 ) 333 334 self._last_query_time: float | None = None 335 336 # If there seems to be a relatively-recent valid cached info for this 337 # tournament, use it. Otherwise we'll kick off a query ourselves. 338 if ( 339 self._tournament_id in bui.app.classic.accounts.tournament_info 340 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 341 'valid' 342 ] 343 and ( 344 bui.apptime() 345 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 346 'timeReceived' 347 ] 348 < 60 * 5 349 ) 350 ): 351 try: 352 info = bui.app.classic.accounts.tournament_info[ 353 self._tournament_id 354 ] 355 self._seconds_remaining = max( 356 0, 357 info['timeRemaining'] 358 - int((bui.apptime() - info['timeReceived'])), 359 ) 360 self._have_valid_data = True 361 self._last_query_time = bui.apptime() 362 except Exception: 363 logging.exception('Error using valid tourney data.') 364 self._have_valid_data = False 365 else: 366 self._have_valid_data = False 367 368 self._fg_state = bui.app.fg_state 369 self._running_query = False 370 self._update_timer = bui.AppTimer( 371 1.0, bui.WeakCall(self._update), repeat=True 372 ) 373 self._update() 374 self._restore_state() 375 376 def _on_tournament_query_response( 377 self, data: dict[str, Any] | None 378 ) -> None: 379 assert bui.app.classic is not None 380 accounts = bui.app.classic.accounts 381 self._running_query = False 382 if data is not None: 383 data = data['t'] # This used to be the whole payload. 384 accounts.cache_tournament_info(data) 385 self._seconds_remaining = accounts.tournament_info[ 386 self._tournament_id 387 ]['timeRemaining'] 388 self._have_valid_data = True 389 390 def _save_state(self) -> None: 391 if not self.root_widget: 392 return 393 sel = self.root_widget.get_selected_child() 394 if sel == self._pay_with_ad_btn: 395 sel_name = 'Ad' 396 elif sel == self._practice_button: 397 sel_name = 'Practice' 398 else: 399 sel_name = 'Tickets' 400 cfg = bui.app.config 401 cfg['Tournament Pay Selection'] = sel_name 402 cfg.commit() 403 404 def _restore_state(self) -> None: 405 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 406 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 407 sel = self._pay_with_ad_btn 408 elif sel_name == 'Practice' and self._practice_button is not None: 409 sel = self._practice_button 410 else: 411 sel = self._pay_with_tickets_button 412 bui.containerwidget(edit=self.root_widget, selected_child=sel) 413 414 def _update(self) -> None: 415 plus = bui.app.plus 416 assert plus is not None 417 418 # We may outlive our widgets. 419 if not self.root_widget: 420 return 421 422 # If we've been foregrounded/backgrounded we need to re-grab data. 423 if self._fg_state != bui.app.fg_state: 424 self._fg_state = bui.app.fg_state 425 self._have_valid_data = False 426 427 # If we need to run another tournament query, do so. 428 if not self._running_query and ( 429 (self._last_query_time is None) 430 or (not self._have_valid_data) 431 or (bui.apptime() - self._last_query_time > 30.0) 432 ): 433 plus.tournament_query( 434 args={ 435 'source': ( 436 'entry window' 437 if self._tournament_activity is None 438 else 'retry entry window' 439 ) 440 }, 441 callback=bui.WeakCall(self._on_tournament_query_response), 442 ) 443 self._last_query_time = bui.apptime() 444 self._running_query = True 445 446 # Grab the latest info on our tourney. 447 assert bui.app.classic is not None 448 self._tournament_info = bui.app.classic.accounts.tournament_info[ 449 self._tournament_id 450 ] 451 452 # If we don't have valid data always show a '-' for time. 453 if not self._have_valid_data: 454 bui.textwidget(edit=self._time_remaining_text, text='-') 455 else: 456 if self._seconds_remaining is not None: 457 self._seconds_remaining = max(0, self._seconds_remaining - 1) 458 bui.textwidget( 459 edit=self._time_remaining_text, 460 text=bui.timestring(self._seconds_remaining, centi=False), 461 ) 462 463 # Keep price up-to-date and update the button with it. 464 self._purchase_price = plus.get_v1_account_misc_read_val( 465 self._purchase_price_name, None 466 ) 467 468 bui.textwidget( 469 edit=self._ticket_cost_text, 470 text=( 471 bui.Lstr(resource='getTicketsWindow.freeText') 472 if self._purchase_price == 0 473 else bui.Lstr( 474 resource='getTicketsWindow.ticketsText', 475 subs=[ 476 ( 477 '${COUNT}', 478 ( 479 str(self._purchase_price) 480 if self._purchase_price is not None 481 else '?' 482 ), 483 ) 484 ], 485 ) 486 ), 487 position=( 488 self._ticket_cost_text_position_free 489 if self._purchase_price == 0 490 else self._ticket_cost_text_position 491 ), 492 scale=1.0 if self._purchase_price == 0 else 0.6, 493 ) 494 495 bui.textwidget( 496 edit=self._free_plays_remaining_text, 497 text=( 498 '' 499 if ( 500 self._tournament_info['freeTriesRemaining'] in [None, 0] 501 or self._purchase_price != 0 502 ) 503 else '' + str(self._tournament_info['freeTriesRemaining']) 504 ), 505 ) 506 507 bui.imagewidget( 508 edit=self._ticket_img, 509 opacity=0.2 if self._purchase_price == 0 else 1.0, 510 position=( 511 self._ticket_img_pos_free 512 if self._purchase_price == 0 513 else self._ticket_img_pos 514 ), 515 ) 516 517 if self._do_ad_btn: 518 enabled = plus.have_incentivized_ad() 519 have_ad_tries_remaining = ( 520 self._tournament_info['adTriesRemaining'] is not None 521 and self._tournament_info['adTriesRemaining'] > 0 522 ) 523 bui.textwidget( 524 edit=self._ad_text, 525 position=( 526 self._ad_text_position_remaining 527 if have_ad_tries_remaining 528 else self._ad_text_position 529 ), 530 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 531 ) 532 bui.imagewidget( 533 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 534 ) 535 bui.buttonwidget( 536 edit=self._pay_with_ad_btn, 537 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 538 ) 539 ad_plays_remaining_text = ( 540 '' 541 if not have_ad_tries_remaining 542 else '' + str(self._tournament_info['adTriesRemaining']) 543 ) 544 bui.textwidget( 545 edit=self._ad_plays_remaining_text, 546 text=ad_plays_remaining_text, 547 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 548 ) 549 550 try: 551 t_str = str(plus.get_v1_account_ticket_count()) 552 except Exception: 553 t_str = '?' 554 if self._get_tickets_button: 555 bui.buttonwidget( 556 edit=self._get_tickets_button, 557 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 558 ) 559 if self._ticket_count_text: 560 bui.textwidget( 561 edit=self._ticket_count_text, 562 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 563 ) 564 565 def _launch(self, practice: bool = False) -> None: 566 assert bui.app.classic is not None 567 if self._launched: 568 return 569 self._launched = True 570 launched = False 571 572 # If they gave us an existing, non-consistent 573 # practice activity, just restart it. 574 if ( 575 self._tournament_activity is not None 576 and not practice == self._tournament_activity.session.submit_score 577 ): 578 try: 579 if not practice: 580 bui.apptimer(0.1, bui.getsound('cashRegister').play) 581 bui.screenmessage( 582 bui.Lstr( 583 translate=( 584 'serverResponses', 585 'Entering tournament...', 586 ) 587 ), 588 color=(0, 1, 0), 589 ) 590 bui.apptimer(0 if practice else 0.3, self._transition_out) 591 launched = True 592 with self._tournament_activity.context: 593 self._tournament_activity.end( 594 {'outcome': 'restart'}, force=True 595 ) 596 597 # We can hit exceptions here if _tournament_activity ends before 598 # our restart attempt happens. 599 # In this case we'll fall back to launching a new session. 600 # This is not ideal since players will have to rejoin, etc., 601 # but it works for now. 602 except Exception: 603 logging.exception('Error restarting tournament activity.') 604 605 # If we had no existing activity (or were unable to restart it) 606 # launch a new session. 607 if not launched: 608 if not practice: 609 bui.apptimer(0.1, bui.getsound('cashRegister').play) 610 bui.screenmessage( 611 bui.Lstr( 612 translate=('serverResponses', 'Entering tournament...') 613 ), 614 color=(0, 1, 0), 615 ) 616 bui.apptimer( 617 0 if practice else 1.0, 618 lambda: ( 619 bui.app.classic.launch_coop_game( 620 self._tournament_info['game'], 621 args={ 622 'min_players': self._tournament_info['minPlayers'], 623 'max_players': self._tournament_info['maxPlayers'], 624 'tournament_id': self._tournament_id, 625 'submit_score': not practice, 626 }, 627 ) 628 if bui.app.classic is not None 629 else None 630 ), 631 ) 632 bui.apptimer(0 if practice else 1.25, self._transition_out) 633 634 def _on_pay_with_tickets_press(self) -> None: 635 from bauiv1lib import getcurrency 636 637 plus = bui.app.plus 638 assert plus is not None 639 640 # If we're already entering, ignore. 641 if self._entering: 642 return 643 644 if not self._have_valid_data: 645 bui.screenmessage( 646 bui.Lstr(resource='tournamentCheckingStateText'), 647 color=(1, 0, 0), 648 ) 649 bui.getsound('error').play() 650 return 651 652 # If we don't have a price. 653 if self._purchase_price is None: 654 bui.screenmessage( 655 bui.Lstr(resource='tournamentCheckingStateText'), 656 color=(1, 0, 0), 657 ) 658 bui.getsound('error').play() 659 return 660 661 # Deny if it looks like the tourney has ended. 662 if self._seconds_remaining == 0: 663 bui.screenmessage( 664 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 665 ) 666 bui.getsound('error').play() 667 return 668 669 # Deny if we don't have enough tickets. 670 ticket_count: int | None 671 try: 672 ticket_count = plus.get_v1_account_ticket_count() 673 except Exception: 674 # FIXME: should add a bui.NotSignedInError we can use here. 675 ticket_count = None 676 ticket_cost = self._purchase_price 677 if ticket_count is not None and ticket_count < ticket_cost: 678 getcurrency.show_get_tickets_prompt() 679 bui.getsound('error').play() 680 self._transition_out() 681 return 682 683 cur_time = bui.apptime() 684 self._last_ticket_press_time = cur_time 685 assert isinstance(ticket_cost, int) 686 plus.in_game_purchase(self._purchase_name, ticket_cost) 687 688 self._entering = True 689 plus.add_v1_account_transaction( 690 { 691 'type': 'ENTER_TOURNAMENT', 692 'fee': self._fee, 693 'tournamentID': self._tournament_id, 694 } 695 ) 696 plus.run_v1_account_transactions() 697 self._launch() 698 699 def _on_pay_with_ad_press(self) -> None: 700 # If we're already entering, ignore. 701 if self._entering: 702 return 703 704 if not self._have_valid_data: 705 bui.screenmessage( 706 bui.Lstr(resource='tournamentCheckingStateText'), 707 color=(1, 0, 0), 708 ) 709 bui.getsound('error').play() 710 return 711 712 # Deny if it looks like the tourney has ended. 713 if self._seconds_remaining == 0: 714 bui.screenmessage( 715 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 716 ) 717 bui.getsound('error').play() 718 return 719 720 cur_time = bui.apptime() 721 if cur_time - self._last_ad_press_time > 5.0: 722 self._last_ad_press_time = cur_time 723 assert bui.app.classic is not None 724 bui.app.classic.ads.show_ad_2( 725 'tournament_entry', 726 on_completion_call=bui.WeakCall(self._on_ad_complete), 727 ) 728 729 def _on_practice_press(self) -> None: 730 plus = bui.app.plus 731 assert plus is not None 732 733 # If we're already entering, ignore. 734 if self._entering: 735 return 736 737 # Deny if it looks like the tourney has ended. 738 if self._seconds_remaining == 0: 739 bui.screenmessage( 740 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 741 ) 742 bui.getsound('error').play() 743 return 744 745 self._entering = True 746 self._launch(practice=True) 747 748 def _on_ad_complete(self, actually_showed: bool) -> None: 749 plus = bui.app.plus 750 assert plus is not None 751 752 # Make sure any transactions the ad added got locally applied 753 # (rewards added, etc.). 754 plus.run_v1_account_transactions() 755 756 # If we're already entering the tourney, ignore. 757 if self._entering: 758 return 759 760 if not actually_showed: 761 return 762 763 # This should have awarded us the tournament_entry_ad purchase; 764 # make sure that's present. 765 # (otherwise the server will ignore our tournament entry anyway) 766 if not plus.get_purchased('tournament_entry_ad'): 767 print('no tournament_entry_ad purchase present in _on_ad_complete') 768 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 769 bui.getsound('error').play() 770 return 771 772 self._entering = True 773 plus.add_v1_account_transaction( 774 { 775 'type': 'ENTER_TOURNAMENT', 776 'fee': 'ad', 777 'tournamentID': self._tournament_id, 778 } 779 ) 780 plus.run_v1_account_transactions() 781 self._launch() 782 783 def _on_get_tickets_press(self) -> None: 784 from bauiv1lib import getcurrency 785 786 # If we're already entering, ignore presses. 787 if self._entering: 788 return 789 790 # Bring up get-tickets window and then kill ourself (we're on the 791 # overlay layer so we'd show up above it). 792 getcurrency.GetCurrencyWindow( 793 modal=True, origin_widget=self._get_tickets_button 794 ) 795 self._transition_out() 796 797 def _on_cancel(self) -> None: 798 plus = bui.app.plus 799 assert plus is not None 800 # Don't allow canceling for several seconds after poking an enter 801 # button if it looks like we're waiting on a purchase or entering 802 # the tournament. 803 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 804 plus.have_outstanding_v1_account_transactions() 805 or plus.get_purchased(self._purchase_name) 806 or self._entering 807 ): 808 bui.getsound('error').play() 809 return 810 self._transition_out() 811 812 def _transition_out(self) -> None: 813 if not self.root_widget: 814 return 815 if not self._transitioning_out: 816 self._transitioning_out = True 817 self._save_state() 818 bui.containerwidget(edit=self.root_widget, transition='out_scale') 819 if self._on_close_call is not None: 820 self._on_close_call() 821 822 @override 823 def on_popup_cancel(self) -> None: 824 bui.getsound('swish').play() 825 self._on_cancel()
19class TournamentEntryWindow(PopupWindow): 20 """Popup window for entering tournaments.""" 21 22 def __init__( 23 self, 24 tournament_id: str, 25 tournament_activity: bs.Activity | None = None, 26 position: tuple[float, float] = (0.0, 0.0), 27 delegate: Any = None, 28 scale: float | None = None, 29 offset: tuple[float, float] = (0.0, 0.0), 30 on_close_call: Callable[[], Any] | None = None, 31 ): 32 # Needs some tidying. 33 # pylint: disable=too-many-locals 34 # pylint: disable=too-many-branches 35 # pylint: disable=too-many-statements 36 37 assert bui.app.classic is not None 38 assert bui.app.plus 39 bui.set_analytics_screen('Tournament Entry Window') 40 41 self._tournament_id = tournament_id 42 self._tournament_info = bui.app.classic.accounts.tournament_info[ 43 self._tournament_id 44 ] 45 46 # Set a few vars depending on the tourney fee. 47 self._fee = self._tournament_info['fee'] 48 self._allow_ads = self._tournament_info['allowAds'] 49 if self._fee == 4: 50 self._purchase_name = 'tournament_entry_4' 51 self._purchase_price_name = 'price.tournament_entry_4' 52 elif self._fee == 3: 53 self._purchase_name = 'tournament_entry_3' 54 self._purchase_price_name = 'price.tournament_entry_3' 55 elif self._fee == 2: 56 self._purchase_name = 'tournament_entry_2' 57 self._purchase_price_name = 'price.tournament_entry_2' 58 elif self._fee == 1: 59 self._purchase_name = 'tournament_entry_1' 60 self._purchase_price_name = 'price.tournament_entry_1' 61 else: 62 if self._fee != 0: 63 raise ValueError('invalid fee: ' + str(self._fee)) 64 self._purchase_name = 'tournament_entry_0' 65 self._purchase_price_name = 'price.tournament_entry_0' 66 67 self._purchase_price: int | None = None 68 69 self._on_close_call = on_close_call 70 if scale is None: 71 uiscale = bui.app.ui_v1.uiscale 72 scale = ( 73 2.3 74 if uiscale is bui.UIScale.SMALL 75 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 76 ) 77 self._delegate = delegate 78 self._transitioning_out = False 79 80 self._tournament_activity = tournament_activity 81 82 self._width: float = 340.0 83 self._height: float = 225.0 84 85 bg_color = (0.5, 0.4, 0.6) 86 87 # Show the practice button as long as we're not 88 # restarting while on a paid tournament run. 89 self._do_practice = ( 90 self._tournament_activity is None 91 and bui.app.config.get('tournament_practice_enabled', False) 92 ) 93 94 off_p = 0 if not self._do_practice else 48 95 self._height += off_p * 0.933 96 97 # Creates our root_widget. 98 super().__init__( 99 position=position, 100 size=(self._width, self._height), 101 scale=scale, 102 bg_color=bg_color, 103 offset=offset, 104 toolbar_visibility='menu_currency', 105 ) 106 107 self._last_ad_press_time = -9999.0 108 self._last_ticket_press_time = -9999.0 109 self._entering = False 110 self._launched = False 111 112 # Show the ad button only if we support ads *and* it has a level 1 fee. 113 self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads 114 115 x_offs = 0 if self._do_ad_btn else 85 116 117 self._cancel_button = bui.buttonwidget( 118 parent=self.root_widget, 119 position=(40, self._height - 34), 120 size=(60, 60), 121 scale=0.5, 122 label='', 123 color=bg_color, 124 on_activate_call=self._on_cancel, 125 autoselect=True, 126 icon=bui.gettexture('crossOut'), 127 iconscale=1.2, 128 ) 129 130 self._title_text = bui.textwidget( 131 parent=self.root_widget, 132 position=(self._width * 0.5, self._height - 20), 133 size=(0, 0), 134 h_align='center', 135 v_align='center', 136 scale=0.6, 137 text=bui.Lstr(resource='tournamentEntryText'), 138 maxwidth=180, 139 color=(1, 1, 1, 0.4), 140 ) 141 142 btn = self._pay_with_tickets_button = bui.buttonwidget( 143 parent=self.root_widget, 144 position=(30 + x_offs, 60 + off_p), 145 autoselect=True, 146 button_type='square', 147 size=(120, 120), 148 label='', 149 on_activate_call=self._on_pay_with_tickets_press, 150 ) 151 self._ticket_img_pos = (50 + x_offs, 94 + off_p) 152 self._ticket_img_pos_free = (50 + x_offs, 80 + off_p) 153 self._ticket_img = bui.imagewidget( 154 parent=self.root_widget, 155 draw_controller=btn, 156 size=(80, 80), 157 position=self._ticket_img_pos, 158 texture=bui.gettexture('tickets'), 159 ) 160 self._ticket_cost_text_position = (87 + x_offs, 88 + off_p) 161 self._ticket_cost_text_position_free = (87 + x_offs, 120 + off_p) 162 self._ticket_cost_text = bui.textwidget( 163 parent=self.root_widget, 164 draw_controller=btn, 165 position=self._ticket_cost_text_position, 166 size=(0, 0), 167 h_align='center', 168 v_align='center', 169 scale=0.6, 170 text='', 171 maxwidth=95, 172 color=(0, 1, 0), 173 ) 174 self._free_plays_remaining_text = bui.textwidget( 175 parent=self.root_widget, 176 draw_controller=btn, 177 position=(87 + x_offs, 78 + off_p), 178 size=(0, 0), 179 h_align='center', 180 v_align='center', 181 scale=0.33, 182 text='', 183 maxwidth=95, 184 color=(0, 0.8, 0), 185 ) 186 self._pay_with_ad_btn: bui.Widget | None 187 if self._do_ad_btn: 188 btn = self._pay_with_ad_btn = bui.buttonwidget( 189 parent=self.root_widget, 190 position=(190, 60 + off_p), 191 autoselect=True, 192 button_type='square', 193 size=(120, 120), 194 label='', 195 on_activate_call=self._on_pay_with_ad_press, 196 ) 197 self._pay_with_ad_img = bui.imagewidget( 198 parent=self.root_widget, 199 draw_controller=btn, 200 size=(80, 80), 201 position=(210, 94 + off_p), 202 texture=bui.gettexture('tv'), 203 ) 204 205 self._ad_text_position = (251, 88 + off_p) 206 self._ad_text_position_remaining = (251, 92 + off_p) 207 have_ad_tries_remaining = ( 208 self._tournament_info['adTriesRemaining'] is not None 209 ) 210 self._ad_text = bui.textwidget( 211 parent=self.root_widget, 212 draw_controller=btn, 213 position=( 214 self._ad_text_position_remaining 215 if have_ad_tries_remaining 216 else self._ad_text_position 217 ), 218 size=(0, 0), 219 h_align='center', 220 v_align='center', 221 scale=0.6, 222 # Note: AdMob now requires rewarded ad usage 223 # specifically says 'Ad' in it. 224 text=bui.Lstr(resource='watchAnAdText'), 225 maxwidth=95, 226 color=(0, 1, 0), 227 ) 228 ad_plays_remaining_text = ( 229 '' 230 if not have_ad_tries_remaining 231 else '' + str(self._tournament_info['adTriesRemaining']) 232 ) 233 self._ad_plays_remaining_text = bui.textwidget( 234 parent=self.root_widget, 235 draw_controller=btn, 236 position=(251, 78 + off_p), 237 size=(0, 0), 238 h_align='center', 239 v_align='center', 240 scale=0.33, 241 text=ad_plays_remaining_text, 242 maxwidth=95, 243 color=(0, 0.8, 0), 244 ) 245 246 bui.textwidget( 247 parent=self.root_widget, 248 position=(self._width * 0.5, 120 + off_p), 249 size=(0, 0), 250 h_align='center', 251 v_align='center', 252 scale=0.6, 253 text=bui.Lstr( 254 resource='orText', subs=[('${A}', ''), ('${B}', '')] 255 ), 256 maxwidth=35, 257 color=(1, 1, 1, 0.5), 258 ) 259 else: 260 self._pay_with_ad_btn = None 261 262 btn_size = (150, 45) 263 btn_pos = (self._width / 2 - btn_size[0] / 2, self._width / 2 - 110) 264 self._practice_button = None 265 if self._do_practice: 266 self._practice_button = bui.buttonwidget( 267 parent=self.root_widget, 268 position=btn_pos, 269 autoselect=True, 270 size=btn_size, 271 label=bui.Lstr(resource='practiceText'), 272 on_activate_call=self._on_practice_press, 273 ) 274 275 self._get_tickets_button: bui.Widget | None = None 276 self._ticket_count_text: bui.Widget | None = None 277 if not bui.app.ui_v1.use_toolbars: 278 if bui.app.classic.allow_ticket_purchases: 279 self._get_tickets_button = bui.buttonwidget( 280 parent=self.root_widget, 281 position=(self._width - 190 + 105, self._height - 34), 282 autoselect=True, 283 scale=0.5, 284 size=(120, 60), 285 textcolor=(0.2, 1, 0.2), 286 label=bui.charstr(bui.SpecialChar.TICKET), 287 color=(0.65, 0.5, 0.8), 288 on_activate_call=self._on_get_tickets_press, 289 ) 290 else: 291 self._ticket_count_text = bui.textwidget( 292 parent=self.root_widget, 293 scale=0.5, 294 position=(self._width - 190 + 125, self._height - 34), 295 color=(0.2, 1, 0.2), 296 h_align='center', 297 v_align='center', 298 ) 299 300 self._seconds_remaining = None 301 302 bui.containerwidget( 303 edit=self.root_widget, cancel_button=self._cancel_button 304 ) 305 306 # Let's also ask the server for info about this tournament 307 # (time remaining, etc) so we can show the user time remaining, 308 # disallow entry if time has run out, etc. 309 # xoffs = 104 if bui.app.ui.use_toolbars else 0 310 self._time_remaining_text = bui.textwidget( 311 parent=self.root_widget, 312 position=(self._width / 2, 28), 313 size=(0, 0), 314 h_align='center', 315 v_align='center', 316 text='-', 317 scale=0.65, 318 maxwidth=100, 319 flatness=1.0, 320 color=(0.7, 0.7, 0.7), 321 ) 322 self._time_remaining_label_text = bui.textwidget( 323 parent=self.root_widget, 324 position=(self._width / 2, 45), 325 size=(0, 0), 326 h_align='center', 327 v_align='center', 328 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 329 scale=0.45, 330 flatness=1.0, 331 maxwidth=100, 332 color=(0.7, 0.7, 0.7), 333 ) 334 335 self._last_query_time: float | None = None 336 337 # If there seems to be a relatively-recent valid cached info for this 338 # tournament, use it. Otherwise we'll kick off a query ourselves. 339 if ( 340 self._tournament_id in bui.app.classic.accounts.tournament_info 341 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 342 'valid' 343 ] 344 and ( 345 bui.apptime() 346 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 347 'timeReceived' 348 ] 349 < 60 * 5 350 ) 351 ): 352 try: 353 info = bui.app.classic.accounts.tournament_info[ 354 self._tournament_id 355 ] 356 self._seconds_remaining = max( 357 0, 358 info['timeRemaining'] 359 - int((bui.apptime() - info['timeReceived'])), 360 ) 361 self._have_valid_data = True 362 self._last_query_time = bui.apptime() 363 except Exception: 364 logging.exception('Error using valid tourney data.') 365 self._have_valid_data = False 366 else: 367 self._have_valid_data = False 368 369 self._fg_state = bui.app.fg_state 370 self._running_query = False 371 self._update_timer = bui.AppTimer( 372 1.0, bui.WeakCall(self._update), repeat=True 373 ) 374 self._update() 375 self._restore_state() 376 377 def _on_tournament_query_response( 378 self, data: dict[str, Any] | None 379 ) -> None: 380 assert bui.app.classic is not None 381 accounts = bui.app.classic.accounts 382 self._running_query = False 383 if data is not None: 384 data = data['t'] # This used to be the whole payload. 385 accounts.cache_tournament_info(data) 386 self._seconds_remaining = accounts.tournament_info[ 387 self._tournament_id 388 ]['timeRemaining'] 389 self._have_valid_data = True 390 391 def _save_state(self) -> None: 392 if not self.root_widget: 393 return 394 sel = self.root_widget.get_selected_child() 395 if sel == self._pay_with_ad_btn: 396 sel_name = 'Ad' 397 elif sel == self._practice_button: 398 sel_name = 'Practice' 399 else: 400 sel_name = 'Tickets' 401 cfg = bui.app.config 402 cfg['Tournament Pay Selection'] = sel_name 403 cfg.commit() 404 405 def _restore_state(self) -> None: 406 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 407 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 408 sel = self._pay_with_ad_btn 409 elif sel_name == 'Practice' and self._practice_button is not None: 410 sel = self._practice_button 411 else: 412 sel = self._pay_with_tickets_button 413 bui.containerwidget(edit=self.root_widget, selected_child=sel) 414 415 def _update(self) -> None: 416 plus = bui.app.plus 417 assert plus is not None 418 419 # We may outlive our widgets. 420 if not self.root_widget: 421 return 422 423 # If we've been foregrounded/backgrounded we need to re-grab data. 424 if self._fg_state != bui.app.fg_state: 425 self._fg_state = bui.app.fg_state 426 self._have_valid_data = False 427 428 # If we need to run another tournament query, do so. 429 if not self._running_query and ( 430 (self._last_query_time is None) 431 or (not self._have_valid_data) 432 or (bui.apptime() - self._last_query_time > 30.0) 433 ): 434 plus.tournament_query( 435 args={ 436 'source': ( 437 'entry window' 438 if self._tournament_activity is None 439 else 'retry entry window' 440 ) 441 }, 442 callback=bui.WeakCall(self._on_tournament_query_response), 443 ) 444 self._last_query_time = bui.apptime() 445 self._running_query = True 446 447 # Grab the latest info on our tourney. 448 assert bui.app.classic is not None 449 self._tournament_info = bui.app.classic.accounts.tournament_info[ 450 self._tournament_id 451 ] 452 453 # If we don't have valid data always show a '-' for time. 454 if not self._have_valid_data: 455 bui.textwidget(edit=self._time_remaining_text, text='-') 456 else: 457 if self._seconds_remaining is not None: 458 self._seconds_remaining = max(0, self._seconds_remaining - 1) 459 bui.textwidget( 460 edit=self._time_remaining_text, 461 text=bui.timestring(self._seconds_remaining, centi=False), 462 ) 463 464 # Keep price up-to-date and update the button with it. 465 self._purchase_price = plus.get_v1_account_misc_read_val( 466 self._purchase_price_name, None 467 ) 468 469 bui.textwidget( 470 edit=self._ticket_cost_text, 471 text=( 472 bui.Lstr(resource='getTicketsWindow.freeText') 473 if self._purchase_price == 0 474 else bui.Lstr( 475 resource='getTicketsWindow.ticketsText', 476 subs=[ 477 ( 478 '${COUNT}', 479 ( 480 str(self._purchase_price) 481 if self._purchase_price is not None 482 else '?' 483 ), 484 ) 485 ], 486 ) 487 ), 488 position=( 489 self._ticket_cost_text_position_free 490 if self._purchase_price == 0 491 else self._ticket_cost_text_position 492 ), 493 scale=1.0 if self._purchase_price == 0 else 0.6, 494 ) 495 496 bui.textwidget( 497 edit=self._free_plays_remaining_text, 498 text=( 499 '' 500 if ( 501 self._tournament_info['freeTriesRemaining'] in [None, 0] 502 or self._purchase_price != 0 503 ) 504 else '' + str(self._tournament_info['freeTriesRemaining']) 505 ), 506 ) 507 508 bui.imagewidget( 509 edit=self._ticket_img, 510 opacity=0.2 if self._purchase_price == 0 else 1.0, 511 position=( 512 self._ticket_img_pos_free 513 if self._purchase_price == 0 514 else self._ticket_img_pos 515 ), 516 ) 517 518 if self._do_ad_btn: 519 enabled = plus.have_incentivized_ad() 520 have_ad_tries_remaining = ( 521 self._tournament_info['adTriesRemaining'] is not None 522 and self._tournament_info['adTriesRemaining'] > 0 523 ) 524 bui.textwidget( 525 edit=self._ad_text, 526 position=( 527 self._ad_text_position_remaining 528 if have_ad_tries_remaining 529 else self._ad_text_position 530 ), 531 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 532 ) 533 bui.imagewidget( 534 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 535 ) 536 bui.buttonwidget( 537 edit=self._pay_with_ad_btn, 538 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 539 ) 540 ad_plays_remaining_text = ( 541 '' 542 if not have_ad_tries_remaining 543 else '' + str(self._tournament_info['adTriesRemaining']) 544 ) 545 bui.textwidget( 546 edit=self._ad_plays_remaining_text, 547 text=ad_plays_remaining_text, 548 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 549 ) 550 551 try: 552 t_str = str(plus.get_v1_account_ticket_count()) 553 except Exception: 554 t_str = '?' 555 if self._get_tickets_button: 556 bui.buttonwidget( 557 edit=self._get_tickets_button, 558 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 559 ) 560 if self._ticket_count_text: 561 bui.textwidget( 562 edit=self._ticket_count_text, 563 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 564 ) 565 566 def _launch(self, practice: bool = False) -> None: 567 assert bui.app.classic is not None 568 if self._launched: 569 return 570 self._launched = True 571 launched = False 572 573 # If they gave us an existing, non-consistent 574 # practice activity, just restart it. 575 if ( 576 self._tournament_activity is not None 577 and not practice == self._tournament_activity.session.submit_score 578 ): 579 try: 580 if not practice: 581 bui.apptimer(0.1, bui.getsound('cashRegister').play) 582 bui.screenmessage( 583 bui.Lstr( 584 translate=( 585 'serverResponses', 586 'Entering tournament...', 587 ) 588 ), 589 color=(0, 1, 0), 590 ) 591 bui.apptimer(0 if practice else 0.3, self._transition_out) 592 launched = True 593 with self._tournament_activity.context: 594 self._tournament_activity.end( 595 {'outcome': 'restart'}, force=True 596 ) 597 598 # We can hit exceptions here if _tournament_activity ends before 599 # our restart attempt happens. 600 # In this case we'll fall back to launching a new session. 601 # This is not ideal since players will have to rejoin, etc., 602 # but it works for now. 603 except Exception: 604 logging.exception('Error restarting tournament activity.') 605 606 # If we had no existing activity (or were unable to restart it) 607 # launch a new session. 608 if not launched: 609 if not practice: 610 bui.apptimer(0.1, bui.getsound('cashRegister').play) 611 bui.screenmessage( 612 bui.Lstr( 613 translate=('serverResponses', 'Entering tournament...') 614 ), 615 color=(0, 1, 0), 616 ) 617 bui.apptimer( 618 0 if practice else 1.0, 619 lambda: ( 620 bui.app.classic.launch_coop_game( 621 self._tournament_info['game'], 622 args={ 623 'min_players': self._tournament_info['minPlayers'], 624 'max_players': self._tournament_info['maxPlayers'], 625 'tournament_id': self._tournament_id, 626 'submit_score': not practice, 627 }, 628 ) 629 if bui.app.classic is not None 630 else None 631 ), 632 ) 633 bui.apptimer(0 if practice else 1.25, self._transition_out) 634 635 def _on_pay_with_tickets_press(self) -> None: 636 from bauiv1lib import getcurrency 637 638 plus = bui.app.plus 639 assert plus is not None 640 641 # If we're already entering, ignore. 642 if self._entering: 643 return 644 645 if not self._have_valid_data: 646 bui.screenmessage( 647 bui.Lstr(resource='tournamentCheckingStateText'), 648 color=(1, 0, 0), 649 ) 650 bui.getsound('error').play() 651 return 652 653 # If we don't have a price. 654 if self._purchase_price is None: 655 bui.screenmessage( 656 bui.Lstr(resource='tournamentCheckingStateText'), 657 color=(1, 0, 0), 658 ) 659 bui.getsound('error').play() 660 return 661 662 # Deny if it looks like the tourney has ended. 663 if self._seconds_remaining == 0: 664 bui.screenmessage( 665 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 666 ) 667 bui.getsound('error').play() 668 return 669 670 # Deny if we don't have enough tickets. 671 ticket_count: int | None 672 try: 673 ticket_count = plus.get_v1_account_ticket_count() 674 except Exception: 675 # FIXME: should add a bui.NotSignedInError we can use here. 676 ticket_count = None 677 ticket_cost = self._purchase_price 678 if ticket_count is not None and ticket_count < ticket_cost: 679 getcurrency.show_get_tickets_prompt() 680 bui.getsound('error').play() 681 self._transition_out() 682 return 683 684 cur_time = bui.apptime() 685 self._last_ticket_press_time = cur_time 686 assert isinstance(ticket_cost, int) 687 plus.in_game_purchase(self._purchase_name, ticket_cost) 688 689 self._entering = True 690 plus.add_v1_account_transaction( 691 { 692 'type': 'ENTER_TOURNAMENT', 693 'fee': self._fee, 694 'tournamentID': self._tournament_id, 695 } 696 ) 697 plus.run_v1_account_transactions() 698 self._launch() 699 700 def _on_pay_with_ad_press(self) -> None: 701 # If we're already entering, ignore. 702 if self._entering: 703 return 704 705 if not self._have_valid_data: 706 bui.screenmessage( 707 bui.Lstr(resource='tournamentCheckingStateText'), 708 color=(1, 0, 0), 709 ) 710 bui.getsound('error').play() 711 return 712 713 # Deny if it looks like the tourney has ended. 714 if self._seconds_remaining == 0: 715 bui.screenmessage( 716 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 717 ) 718 bui.getsound('error').play() 719 return 720 721 cur_time = bui.apptime() 722 if cur_time - self._last_ad_press_time > 5.0: 723 self._last_ad_press_time = cur_time 724 assert bui.app.classic is not None 725 bui.app.classic.ads.show_ad_2( 726 'tournament_entry', 727 on_completion_call=bui.WeakCall(self._on_ad_complete), 728 ) 729 730 def _on_practice_press(self) -> None: 731 plus = bui.app.plus 732 assert plus is not None 733 734 # If we're already entering, ignore. 735 if self._entering: 736 return 737 738 # Deny if it looks like the tourney has ended. 739 if self._seconds_remaining == 0: 740 bui.screenmessage( 741 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 742 ) 743 bui.getsound('error').play() 744 return 745 746 self._entering = True 747 self._launch(practice=True) 748 749 def _on_ad_complete(self, actually_showed: bool) -> None: 750 plus = bui.app.plus 751 assert plus is not None 752 753 # Make sure any transactions the ad added got locally applied 754 # (rewards added, etc.). 755 plus.run_v1_account_transactions() 756 757 # If we're already entering the tourney, ignore. 758 if self._entering: 759 return 760 761 if not actually_showed: 762 return 763 764 # This should have awarded us the tournament_entry_ad purchase; 765 # make sure that's present. 766 # (otherwise the server will ignore our tournament entry anyway) 767 if not plus.get_purchased('tournament_entry_ad'): 768 print('no tournament_entry_ad purchase present in _on_ad_complete') 769 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 770 bui.getsound('error').play() 771 return 772 773 self._entering = True 774 plus.add_v1_account_transaction( 775 { 776 'type': 'ENTER_TOURNAMENT', 777 'fee': 'ad', 778 'tournamentID': self._tournament_id, 779 } 780 ) 781 plus.run_v1_account_transactions() 782 self._launch() 783 784 def _on_get_tickets_press(self) -> None: 785 from bauiv1lib import getcurrency 786 787 # If we're already entering, ignore presses. 788 if self._entering: 789 return 790 791 # Bring up get-tickets window and then kill ourself (we're on the 792 # overlay layer so we'd show up above it). 793 getcurrency.GetCurrencyWindow( 794 modal=True, origin_widget=self._get_tickets_button 795 ) 796 self._transition_out() 797 798 def _on_cancel(self) -> None: 799 plus = bui.app.plus 800 assert plus is not None 801 # Don't allow canceling for several seconds after poking an enter 802 # button if it looks like we're waiting on a purchase or entering 803 # the tournament. 804 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 805 plus.have_outstanding_v1_account_transactions() 806 or plus.get_purchased(self._purchase_name) 807 or self._entering 808 ): 809 bui.getsound('error').play() 810 return 811 self._transition_out() 812 813 def _transition_out(self) -> None: 814 if not self.root_widget: 815 return 816 if not self._transitioning_out: 817 self._transitioning_out = True 818 self._save_state() 819 bui.containerwidget(edit=self.root_widget, transition='out_scale') 820 if self._on_close_call is not None: 821 self._on_close_call() 822 823 @override 824 def on_popup_cancel(self) -> None: 825 bui.getsound('swish').play() 826 self._on_cancel()
Popup window for entering tournaments.
TournamentEntryWindow( tournament_id: str, tournament_activity: bascenev1._activity.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), on_close_call: Optional[Callable[[], Any]] = None)
22 def __init__( 23 self, 24 tournament_id: str, 25 tournament_activity: bs.Activity | None = None, 26 position: tuple[float, float] = (0.0, 0.0), 27 delegate: Any = None, 28 scale: float | None = None, 29 offset: tuple[float, float] = (0.0, 0.0), 30 on_close_call: Callable[[], Any] | None = None, 31 ): 32 # Needs some tidying. 33 # pylint: disable=too-many-locals 34 # pylint: disable=too-many-branches 35 # pylint: disable=too-many-statements 36 37 assert bui.app.classic is not None 38 assert bui.app.plus 39 bui.set_analytics_screen('Tournament Entry Window') 40 41 self._tournament_id = tournament_id 42 self._tournament_info = bui.app.classic.accounts.tournament_info[ 43 self._tournament_id 44 ] 45 46 # Set a few vars depending on the tourney fee. 47 self._fee = self._tournament_info['fee'] 48 self._allow_ads = self._tournament_info['allowAds'] 49 if self._fee == 4: 50 self._purchase_name = 'tournament_entry_4' 51 self._purchase_price_name = 'price.tournament_entry_4' 52 elif self._fee == 3: 53 self._purchase_name = 'tournament_entry_3' 54 self._purchase_price_name = 'price.tournament_entry_3' 55 elif self._fee == 2: 56 self._purchase_name = 'tournament_entry_2' 57 self._purchase_price_name = 'price.tournament_entry_2' 58 elif self._fee == 1: 59 self._purchase_name = 'tournament_entry_1' 60 self._purchase_price_name = 'price.tournament_entry_1' 61 else: 62 if self._fee != 0: 63 raise ValueError('invalid fee: ' + str(self._fee)) 64 self._purchase_name = 'tournament_entry_0' 65 self._purchase_price_name = 'price.tournament_entry_0' 66 67 self._purchase_price: int | None = None 68 69 self._on_close_call = on_close_call 70 if scale is None: 71 uiscale = bui.app.ui_v1.uiscale 72 scale = ( 73 2.3 74 if uiscale is bui.UIScale.SMALL 75 else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 76 ) 77 self._delegate = delegate 78 self._transitioning_out = False 79 80 self._tournament_activity = tournament_activity 81 82 self._width: float = 340.0 83 self._height: float = 225.0 84 85 bg_color = (0.5, 0.4, 0.6) 86 87 # Show the practice button as long as we're not 88 # restarting while on a paid tournament run. 89 self._do_practice = ( 90 self._tournament_activity is None 91 and bui.app.config.get('tournament_practice_enabled', False) 92 ) 93 94 off_p = 0 if not self._do_practice else 48 95 self._height += off_p * 0.933 96 97 # Creates our root_widget. 98 super().__init__( 99 position=position, 100 size=(self._width, self._height), 101 scale=scale, 102 bg_color=bg_color, 103 offset=offset, 104 toolbar_visibility='menu_currency', 105 ) 106 107 self._last_ad_press_time = -9999.0 108 self._last_ticket_press_time = -9999.0 109 self._entering = False 110 self._launched = False 111 112 # Show the ad button only if we support ads *and* it has a level 1 fee. 113 self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads 114 115 x_offs = 0 if self._do_ad_btn else 85 116 117 self._cancel_button = bui.buttonwidget( 118 parent=self.root_widget, 119 position=(40, self._height - 34), 120 size=(60, 60), 121 scale=0.5, 122 label='', 123 color=bg_color, 124 on_activate_call=self._on_cancel, 125 autoselect=True, 126 icon=bui.gettexture('crossOut'), 127 iconscale=1.2, 128 ) 129 130 self._title_text = bui.textwidget( 131 parent=self.root_widget, 132 position=(self._width * 0.5, self._height - 20), 133 size=(0, 0), 134 h_align='center', 135 v_align='center', 136 scale=0.6, 137 text=bui.Lstr(resource='tournamentEntryText'), 138 maxwidth=180, 139 color=(1, 1, 1, 0.4), 140 ) 141 142 btn = self._pay_with_tickets_button = bui.buttonwidget( 143 parent=self.root_widget, 144 position=(30 + x_offs, 60 + off_p), 145 autoselect=True, 146 button_type='square', 147 size=(120, 120), 148 label='', 149 on_activate_call=self._on_pay_with_tickets_press, 150 ) 151 self._ticket_img_pos = (50 + x_offs, 94 + off_p) 152 self._ticket_img_pos_free = (50 + x_offs, 80 + off_p) 153 self._ticket_img = bui.imagewidget( 154 parent=self.root_widget, 155 draw_controller=btn, 156 size=(80, 80), 157 position=self._ticket_img_pos, 158 texture=bui.gettexture('tickets'), 159 ) 160 self._ticket_cost_text_position = (87 + x_offs, 88 + off_p) 161 self._ticket_cost_text_position_free = (87 + x_offs, 120 + off_p) 162 self._ticket_cost_text = bui.textwidget( 163 parent=self.root_widget, 164 draw_controller=btn, 165 position=self._ticket_cost_text_position, 166 size=(0, 0), 167 h_align='center', 168 v_align='center', 169 scale=0.6, 170 text='', 171 maxwidth=95, 172 color=(0, 1, 0), 173 ) 174 self._free_plays_remaining_text = bui.textwidget( 175 parent=self.root_widget, 176 draw_controller=btn, 177 position=(87 + x_offs, 78 + off_p), 178 size=(0, 0), 179 h_align='center', 180 v_align='center', 181 scale=0.33, 182 text='', 183 maxwidth=95, 184 color=(0, 0.8, 0), 185 ) 186 self._pay_with_ad_btn: bui.Widget | None 187 if self._do_ad_btn: 188 btn = self._pay_with_ad_btn = bui.buttonwidget( 189 parent=self.root_widget, 190 position=(190, 60 + off_p), 191 autoselect=True, 192 button_type='square', 193 size=(120, 120), 194 label='', 195 on_activate_call=self._on_pay_with_ad_press, 196 ) 197 self._pay_with_ad_img = bui.imagewidget( 198 parent=self.root_widget, 199 draw_controller=btn, 200 size=(80, 80), 201 position=(210, 94 + off_p), 202 texture=bui.gettexture('tv'), 203 ) 204 205 self._ad_text_position = (251, 88 + off_p) 206 self._ad_text_position_remaining = (251, 92 + off_p) 207 have_ad_tries_remaining = ( 208 self._tournament_info['adTriesRemaining'] is not None 209 ) 210 self._ad_text = bui.textwidget( 211 parent=self.root_widget, 212 draw_controller=btn, 213 position=( 214 self._ad_text_position_remaining 215 if have_ad_tries_remaining 216 else self._ad_text_position 217 ), 218 size=(0, 0), 219 h_align='center', 220 v_align='center', 221 scale=0.6, 222 # Note: AdMob now requires rewarded ad usage 223 # specifically says 'Ad' in it. 224 text=bui.Lstr(resource='watchAnAdText'), 225 maxwidth=95, 226 color=(0, 1, 0), 227 ) 228 ad_plays_remaining_text = ( 229 '' 230 if not have_ad_tries_remaining 231 else '' + str(self._tournament_info['adTriesRemaining']) 232 ) 233 self._ad_plays_remaining_text = bui.textwidget( 234 parent=self.root_widget, 235 draw_controller=btn, 236 position=(251, 78 + off_p), 237 size=(0, 0), 238 h_align='center', 239 v_align='center', 240 scale=0.33, 241 text=ad_plays_remaining_text, 242 maxwidth=95, 243 color=(0, 0.8, 0), 244 ) 245 246 bui.textwidget( 247 parent=self.root_widget, 248 position=(self._width * 0.5, 120 + off_p), 249 size=(0, 0), 250 h_align='center', 251 v_align='center', 252 scale=0.6, 253 text=bui.Lstr( 254 resource='orText', subs=[('${A}', ''), ('${B}', '')] 255 ), 256 maxwidth=35, 257 color=(1, 1, 1, 0.5), 258 ) 259 else: 260 self._pay_with_ad_btn = None 261 262 btn_size = (150, 45) 263 btn_pos = (self._width / 2 - btn_size[0] / 2, self._width / 2 - 110) 264 self._practice_button = None 265 if self._do_practice: 266 self._practice_button = bui.buttonwidget( 267 parent=self.root_widget, 268 position=btn_pos, 269 autoselect=True, 270 size=btn_size, 271 label=bui.Lstr(resource='practiceText'), 272 on_activate_call=self._on_practice_press, 273 ) 274 275 self._get_tickets_button: bui.Widget | None = None 276 self._ticket_count_text: bui.Widget | None = None 277 if not bui.app.ui_v1.use_toolbars: 278 if bui.app.classic.allow_ticket_purchases: 279 self._get_tickets_button = bui.buttonwidget( 280 parent=self.root_widget, 281 position=(self._width - 190 + 105, self._height - 34), 282 autoselect=True, 283 scale=0.5, 284 size=(120, 60), 285 textcolor=(0.2, 1, 0.2), 286 label=bui.charstr(bui.SpecialChar.TICKET), 287 color=(0.65, 0.5, 0.8), 288 on_activate_call=self._on_get_tickets_press, 289 ) 290 else: 291 self._ticket_count_text = bui.textwidget( 292 parent=self.root_widget, 293 scale=0.5, 294 position=(self._width - 190 + 125, self._height - 34), 295 color=(0.2, 1, 0.2), 296 h_align='center', 297 v_align='center', 298 ) 299 300 self._seconds_remaining = None 301 302 bui.containerwidget( 303 edit=self.root_widget, cancel_button=self._cancel_button 304 ) 305 306 # Let's also ask the server for info about this tournament 307 # (time remaining, etc) so we can show the user time remaining, 308 # disallow entry if time has run out, etc. 309 # xoffs = 104 if bui.app.ui.use_toolbars else 0 310 self._time_remaining_text = bui.textwidget( 311 parent=self.root_widget, 312 position=(self._width / 2, 28), 313 size=(0, 0), 314 h_align='center', 315 v_align='center', 316 text='-', 317 scale=0.65, 318 maxwidth=100, 319 flatness=1.0, 320 color=(0.7, 0.7, 0.7), 321 ) 322 self._time_remaining_label_text = bui.textwidget( 323 parent=self.root_widget, 324 position=(self._width / 2, 45), 325 size=(0, 0), 326 h_align='center', 327 v_align='center', 328 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 329 scale=0.45, 330 flatness=1.0, 331 maxwidth=100, 332 color=(0.7, 0.7, 0.7), 333 ) 334 335 self._last_query_time: float | None = None 336 337 # If there seems to be a relatively-recent valid cached info for this 338 # tournament, use it. Otherwise we'll kick off a query ourselves. 339 if ( 340 self._tournament_id in bui.app.classic.accounts.tournament_info 341 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 342 'valid' 343 ] 344 and ( 345 bui.apptime() 346 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 347 'timeReceived' 348 ] 349 < 60 * 5 350 ) 351 ): 352 try: 353 info = bui.app.classic.accounts.tournament_info[ 354 self._tournament_id 355 ] 356 self._seconds_remaining = max( 357 0, 358 info['timeRemaining'] 359 - int((bui.apptime() - info['timeReceived'])), 360 ) 361 self._have_valid_data = True 362 self._last_query_time = bui.apptime() 363 except Exception: 364 logging.exception('Error using valid tourney data.') 365 self._have_valid_data = False 366 else: 367 self._have_valid_data = False 368 369 self._fg_state = bui.app.fg_state 370 self._running_query = False 371 self._update_timer = bui.AppTimer( 372 1.0, bui.WeakCall(self._update), repeat=True 373 ) 374 self._update() 375 self._restore_state()
@override
def
on_popup_cancel(self) -> None:
823 @override 824 def on_popup_cancel(self) -> None: 825 bui.getsound('swish').play() 826 self._on_cancel()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.