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 # pylint: disable=too-many-positional-arguments 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_store_no_back', 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 277 self._seconds_remaining = None 278 279 bui.containerwidget( 280 edit=self.root_widget, cancel_button=self._cancel_button 281 ) 282 283 # Let's also ask the server for info about this tournament 284 # (time remaining, etc) so we can show the user time remaining, 285 # disallow entry if time has run out, etc. 286 # xoffs = 104 if bui.app.ui.use_toolbars else 0 287 self._time_remaining_text = bui.textwidget( 288 parent=self.root_widget, 289 position=(self._width / 2, 28), 290 size=(0, 0), 291 h_align='center', 292 v_align='center', 293 text='-', 294 scale=0.65, 295 maxwidth=100, 296 flatness=1.0, 297 color=(0.7, 0.7, 0.7), 298 ) 299 self._time_remaining_label_text = bui.textwidget( 300 parent=self.root_widget, 301 position=(self._width / 2, 45), 302 size=(0, 0), 303 h_align='center', 304 v_align='center', 305 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 306 scale=0.45, 307 flatness=1.0, 308 maxwidth=100, 309 color=(0.7, 0.7, 0.7), 310 ) 311 312 self._last_query_time: float | None = None 313 314 # If there seems to be a relatively-recent valid cached info for this 315 # tournament, use it. Otherwise we'll kick off a query ourselves. 316 if ( 317 self._tournament_id in bui.app.classic.accounts.tournament_info 318 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 319 'valid' 320 ] 321 and ( 322 bui.apptime() 323 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 324 'timeReceived' 325 ] 326 < 60 * 5 327 ) 328 ): 329 try: 330 info = bui.app.classic.accounts.tournament_info[ 331 self._tournament_id 332 ] 333 self._seconds_remaining = max( 334 0, 335 info['timeRemaining'] 336 - int((bui.apptime() - info['timeReceived'])), 337 ) 338 self._have_valid_data = True 339 self._last_query_time = bui.apptime() 340 except Exception: 341 logging.exception('Error using valid tourney data.') 342 self._have_valid_data = False 343 else: 344 self._have_valid_data = False 345 346 self._fg_state = bui.app.fg_state 347 self._running_query = False 348 self._update_timer = bui.AppTimer( 349 1.0, bui.WeakCall(self._update), repeat=True 350 ) 351 self._update() 352 self._restore_state() 353 354 def _on_tournament_query_response( 355 self, data: dict[str, Any] | None 356 ) -> None: 357 assert bui.app.classic is not None 358 accounts = bui.app.classic.accounts 359 self._running_query = False 360 if data is not None: 361 data = data['t'] # This used to be the whole payload. 362 accounts.cache_tournament_info(data) 363 self._seconds_remaining = accounts.tournament_info[ 364 self._tournament_id 365 ]['timeRemaining'] 366 self._have_valid_data = True 367 368 def _save_state(self) -> None: 369 if not self.root_widget: 370 return 371 sel = self.root_widget.get_selected_child() 372 if sel == self._pay_with_ad_btn: 373 sel_name = 'Ad' 374 elif sel == self._practice_button: 375 sel_name = 'Practice' 376 else: 377 sel_name = 'Tickets' 378 cfg = bui.app.config 379 cfg['Tournament Pay Selection'] = sel_name 380 cfg.commit() 381 382 def _restore_state(self) -> None: 383 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 384 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 385 sel = self._pay_with_ad_btn 386 elif sel_name == 'Practice' and self._practice_button is not None: 387 sel = self._practice_button 388 else: 389 sel = self._pay_with_tickets_button 390 bui.containerwidget(edit=self.root_widget, selected_child=sel) 391 392 def _update(self) -> None: 393 plus = bui.app.plus 394 assert plus is not None 395 396 # We may outlive our widgets. 397 if not self.root_widget: 398 return 399 400 # If we've been foregrounded/backgrounded we need to re-grab data. 401 if self._fg_state != bui.app.fg_state: 402 self._fg_state = bui.app.fg_state 403 self._have_valid_data = False 404 405 # If we need to run another tournament query, do so. 406 if not self._running_query and ( 407 (self._last_query_time is None) 408 or (not self._have_valid_data) 409 or (bui.apptime() - self._last_query_time > 30.0) 410 ): 411 plus.tournament_query( 412 args={ 413 'source': ( 414 'entry window' 415 if self._tournament_activity is None 416 else 'retry entry window' 417 ) 418 }, 419 callback=bui.WeakCall(self._on_tournament_query_response), 420 ) 421 self._last_query_time = bui.apptime() 422 self._running_query = True 423 424 # Grab the latest info on our tourney. 425 assert bui.app.classic is not None 426 self._tournament_info = bui.app.classic.accounts.tournament_info[ 427 self._tournament_id 428 ] 429 430 # If we don't have valid data always show a '-' for time. 431 if not self._have_valid_data: 432 bui.textwidget(edit=self._time_remaining_text, text='-') 433 else: 434 if self._seconds_remaining is not None: 435 self._seconds_remaining = max(0, self._seconds_remaining - 1) 436 bui.textwidget( 437 edit=self._time_remaining_text, 438 text=bui.timestring(self._seconds_remaining, centi=False), 439 ) 440 441 # Keep price up-to-date and update the button with it. 442 self._purchase_price = plus.get_v1_account_misc_read_val( 443 self._purchase_price_name, None 444 ) 445 446 bui.textwidget( 447 edit=self._ticket_cost_text, 448 text=( 449 bui.Lstr(resource='getTicketsWindow.freeText') 450 if self._purchase_price == 0 451 else bui.Lstr( 452 resource='getTicketsWindow.ticketsText', 453 subs=[ 454 ( 455 '${COUNT}', 456 ( 457 str(self._purchase_price) 458 if self._purchase_price is not None 459 else '?' 460 ), 461 ) 462 ], 463 ) 464 ), 465 position=( 466 self._ticket_cost_text_position_free 467 if self._purchase_price == 0 468 else self._ticket_cost_text_position 469 ), 470 scale=1.0 if self._purchase_price == 0 else 0.6, 471 ) 472 473 bui.textwidget( 474 edit=self._free_plays_remaining_text, 475 text=( 476 '' 477 if ( 478 self._tournament_info['freeTriesRemaining'] in [None, 0] 479 or self._purchase_price != 0 480 ) 481 else '' + str(self._tournament_info['freeTriesRemaining']) 482 ), 483 ) 484 485 bui.imagewidget( 486 edit=self._ticket_img, 487 opacity=0.2 if self._purchase_price == 0 else 1.0, 488 position=( 489 self._ticket_img_pos_free 490 if self._purchase_price == 0 491 else self._ticket_img_pos 492 ), 493 ) 494 495 if self._do_ad_btn: 496 enabled = plus.have_incentivized_ad() 497 have_ad_tries_remaining = ( 498 self._tournament_info['adTriesRemaining'] is not None 499 and self._tournament_info['adTriesRemaining'] > 0 500 ) 501 bui.textwidget( 502 edit=self._ad_text, 503 position=( 504 self._ad_text_position_remaining 505 if have_ad_tries_remaining 506 else self._ad_text_position 507 ), 508 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 509 ) 510 bui.imagewidget( 511 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 512 ) 513 bui.buttonwidget( 514 edit=self._pay_with_ad_btn, 515 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 516 ) 517 ad_plays_remaining_text = ( 518 '' 519 if not have_ad_tries_remaining 520 else '' + str(self._tournament_info['adTriesRemaining']) 521 ) 522 bui.textwidget( 523 edit=self._ad_plays_remaining_text, 524 text=ad_plays_remaining_text, 525 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 526 ) 527 528 try: 529 t_str = str(plus.get_v1_account_ticket_count()) 530 except Exception: 531 t_str = '?' 532 if self._get_tickets_button: 533 bui.buttonwidget( 534 edit=self._get_tickets_button, 535 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 536 ) 537 if self._ticket_count_text: 538 bui.textwidget( 539 edit=self._ticket_count_text, 540 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 541 ) 542 543 def _launch(self, practice: bool = False) -> None: 544 assert bui.app.classic is not None 545 if self._launched: 546 return 547 self._launched = True 548 launched = False 549 550 # If they gave us an existing, non-consistent 551 # practice activity, just restart it. 552 if ( 553 self._tournament_activity is not None 554 and not practice == self._tournament_activity.session.submit_score 555 ): 556 try: 557 if not practice: 558 bui.apptimer(0.1, bui.getsound('cashRegister').play) 559 bui.screenmessage( 560 bui.Lstr( 561 translate=( 562 'serverResponses', 563 'Entering tournament...', 564 ) 565 ), 566 color=(0, 1, 0), 567 ) 568 bui.apptimer(0 if practice else 0.3, self._transition_out) 569 launched = True 570 with self._tournament_activity.context: 571 self._tournament_activity.end( 572 {'outcome': 'restart'}, force=True 573 ) 574 575 # We can hit exceptions here if _tournament_activity ends before 576 # our restart attempt happens. 577 # In this case we'll fall back to launching a new session. 578 # This is not ideal since players will have to rejoin, etc., 579 # but it works for now. 580 except Exception: 581 logging.exception('Error restarting tournament activity.') 582 583 # If we had no existing activity (or were unable to restart it) 584 # launch a new session. 585 if not launched: 586 if not practice: 587 bui.apptimer(0.1, bui.getsound('cashRegister').play) 588 bui.screenmessage( 589 bui.Lstr( 590 translate=('serverResponses', 'Entering tournament...') 591 ), 592 color=(0, 1, 0), 593 ) 594 bui.apptimer( 595 0 if practice else 1.0, 596 lambda: ( 597 bui.app.classic.launch_coop_game( 598 self._tournament_info['game'], 599 args={ 600 'min_players': self._tournament_info['minPlayers'], 601 'max_players': self._tournament_info['maxPlayers'], 602 'tournament_id': self._tournament_id, 603 'submit_score': not practice, 604 }, 605 ) 606 if bui.app.classic is not None 607 else None 608 ), 609 ) 610 bui.apptimer(0 if practice else 1.25, self._transition_out) 611 612 def _on_pay_with_tickets_press(self) -> None: 613 # from bauiv1lib import gettickets 614 615 plus = bui.app.plus 616 assert plus is not None 617 618 # If we're already entering, ignore. 619 if self._entering: 620 return 621 622 if not self._have_valid_data: 623 bui.screenmessage( 624 bui.Lstr(resource='tournamentCheckingStateText'), 625 color=(1, 0, 0), 626 ) 627 bui.getsound('error').play() 628 return 629 630 # If we don't have a price. 631 if self._purchase_price is None: 632 bui.screenmessage( 633 bui.Lstr(resource='tournamentCheckingStateText'), 634 color=(1, 0, 0), 635 ) 636 bui.getsound('error').play() 637 return 638 639 # Deny if it looks like the tourney has ended. 640 if self._seconds_remaining == 0: 641 bui.screenmessage( 642 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 643 ) 644 bui.getsound('error').play() 645 return 646 647 # Deny if we don't have enough tickets. 648 ticket_count: int | None 649 try: 650 ticket_count = plus.get_v1_account_ticket_count() 651 except Exception: 652 # FIXME: should add a bui.NotSignedInError we can use here. 653 ticket_count = None 654 ticket_cost = self._purchase_price 655 if ticket_count is not None and ticket_count < ticket_cost: 656 # gettickets.show_get_tickets_prompt() 657 print('FIXME - show not-enough-tickets msg.') 658 bui.getsound('error').play() 659 self._transition_out() 660 return 661 662 cur_time = bui.apptime() 663 self._last_ticket_press_time = cur_time 664 assert isinstance(ticket_cost, int) 665 plus.in_game_purchase(self._purchase_name, ticket_cost) 666 667 self._entering = True 668 plus.add_v1_account_transaction( 669 { 670 'type': 'ENTER_TOURNAMENT', 671 'fee': self._fee, 672 'tournamentID': self._tournament_id, 673 } 674 ) 675 plus.run_v1_account_transactions() 676 self._launch() 677 678 def _on_pay_with_ad_press(self) -> None: 679 # If we're already entering, ignore. 680 if self._entering: 681 return 682 683 if not self._have_valid_data: 684 bui.screenmessage( 685 bui.Lstr(resource='tournamentCheckingStateText'), 686 color=(1, 0, 0), 687 ) 688 bui.getsound('error').play() 689 return 690 691 # Deny if it looks like the tourney has ended. 692 if self._seconds_remaining == 0: 693 bui.screenmessage( 694 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 695 ) 696 bui.getsound('error').play() 697 return 698 699 cur_time = bui.apptime() 700 if cur_time - self._last_ad_press_time > 5.0: 701 self._last_ad_press_time = cur_time 702 assert bui.app.classic is not None 703 bui.app.classic.ads.show_ad_2( 704 'tournament_entry', 705 on_completion_call=bui.WeakCall(self._on_ad_complete), 706 ) 707 708 def _on_practice_press(self) -> None: 709 plus = bui.app.plus 710 assert plus is not None 711 712 # If we're already entering, ignore. 713 if self._entering: 714 return 715 716 # Deny if it looks like the tourney has ended. 717 if self._seconds_remaining == 0: 718 bui.screenmessage( 719 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 720 ) 721 bui.getsound('error').play() 722 return 723 724 self._entering = True 725 self._launch(practice=True) 726 727 def _on_ad_complete(self, actually_showed: bool) -> None: 728 plus = bui.app.plus 729 assert plus is not None 730 731 # Make sure any transactions the ad added got locally applied 732 # (rewards added, etc.). 733 plus.run_v1_account_transactions() 734 735 # If we're already entering the tourney, ignore. 736 if self._entering: 737 return 738 739 if not actually_showed: 740 return 741 742 # This should have awarded us the tournament_entry_ad purchase; 743 # make sure that's present. 744 # (otherwise the server will ignore our tournament entry anyway) 745 if not plus.get_v1_account_product_purchased('tournament_entry_ad'): 746 print('no tournament_entry_ad purchase present in _on_ad_complete') 747 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 748 bui.getsound('error').play() 749 return 750 751 self._entering = True 752 plus.add_v1_account_transaction( 753 { 754 'type': 'ENTER_TOURNAMENT', 755 'fee': 'ad', 756 'tournamentID': self._tournament_id, 757 } 758 ) 759 plus.run_v1_account_transactions() 760 self._launch() 761 762 # def _on_get_tickets_press(self) -> None: 763 # from bauiv1lib import gettickets 764 765 # # If we're already entering, ignore presses. 766 # if self._entering: 767 # return 768 769 # # Bring up get-tickets window and then kill ourself (we're on the 770 # # overlay layer so we'd show up above it). 771 # gettickets.GetTicketsWindow( 772 # modal=True, origin_widget=self._get_tickets_button 773 # ) 774 # self._transition_out() 775 776 def _on_cancel(self) -> None: 777 plus = bui.app.plus 778 assert plus is not None 779 # Don't allow canceling for several seconds after poking an enter 780 # button if it looks like we're waiting on a purchase or entering 781 # the tournament. 782 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 783 plus.have_outstanding_v1_account_transactions() 784 or plus.get_v1_account_product_purchased(self._purchase_name) 785 or self._entering 786 ): 787 bui.getsound('error').play() 788 return 789 self._transition_out() 790 791 def _transition_out(self) -> None: 792 if not self.root_widget: 793 return 794 if not self._transitioning_out: 795 self._transitioning_out = True 796 self._save_state() 797 bui.containerwidget(edit=self.root_widget, transition='out_scale') 798 if self._on_close_call is not None: 799 self._on_close_call() 800 801 @override 802 def on_popup_cancel(self) -> None: 803 bui.getsound('swish').play() 804 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 # pylint: disable=too-many-positional-arguments 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_store_no_back', 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 278 self._seconds_remaining = None 279 280 bui.containerwidget( 281 edit=self.root_widget, cancel_button=self._cancel_button 282 ) 283 284 # Let's also ask the server for info about this tournament 285 # (time remaining, etc) so we can show the user time remaining, 286 # disallow entry if time has run out, etc. 287 # xoffs = 104 if bui.app.ui.use_toolbars else 0 288 self._time_remaining_text = bui.textwidget( 289 parent=self.root_widget, 290 position=(self._width / 2, 28), 291 size=(0, 0), 292 h_align='center', 293 v_align='center', 294 text='-', 295 scale=0.65, 296 maxwidth=100, 297 flatness=1.0, 298 color=(0.7, 0.7, 0.7), 299 ) 300 self._time_remaining_label_text = bui.textwidget( 301 parent=self.root_widget, 302 position=(self._width / 2, 45), 303 size=(0, 0), 304 h_align='center', 305 v_align='center', 306 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 307 scale=0.45, 308 flatness=1.0, 309 maxwidth=100, 310 color=(0.7, 0.7, 0.7), 311 ) 312 313 self._last_query_time: float | None = None 314 315 # If there seems to be a relatively-recent valid cached info for this 316 # tournament, use it. Otherwise we'll kick off a query ourselves. 317 if ( 318 self._tournament_id in bui.app.classic.accounts.tournament_info 319 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 320 'valid' 321 ] 322 and ( 323 bui.apptime() 324 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 325 'timeReceived' 326 ] 327 < 60 * 5 328 ) 329 ): 330 try: 331 info = bui.app.classic.accounts.tournament_info[ 332 self._tournament_id 333 ] 334 self._seconds_remaining = max( 335 0, 336 info['timeRemaining'] 337 - int((bui.apptime() - info['timeReceived'])), 338 ) 339 self._have_valid_data = True 340 self._last_query_time = bui.apptime() 341 except Exception: 342 logging.exception('Error using valid tourney data.') 343 self._have_valid_data = False 344 else: 345 self._have_valid_data = False 346 347 self._fg_state = bui.app.fg_state 348 self._running_query = False 349 self._update_timer = bui.AppTimer( 350 1.0, bui.WeakCall(self._update), repeat=True 351 ) 352 self._update() 353 self._restore_state() 354 355 def _on_tournament_query_response( 356 self, data: dict[str, Any] | None 357 ) -> None: 358 assert bui.app.classic is not None 359 accounts = bui.app.classic.accounts 360 self._running_query = False 361 if data is not None: 362 data = data['t'] # This used to be the whole payload. 363 accounts.cache_tournament_info(data) 364 self._seconds_remaining = accounts.tournament_info[ 365 self._tournament_id 366 ]['timeRemaining'] 367 self._have_valid_data = True 368 369 def _save_state(self) -> None: 370 if not self.root_widget: 371 return 372 sel = self.root_widget.get_selected_child() 373 if sel == self._pay_with_ad_btn: 374 sel_name = 'Ad' 375 elif sel == self._practice_button: 376 sel_name = 'Practice' 377 else: 378 sel_name = 'Tickets' 379 cfg = bui.app.config 380 cfg['Tournament Pay Selection'] = sel_name 381 cfg.commit() 382 383 def _restore_state(self) -> None: 384 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 385 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 386 sel = self._pay_with_ad_btn 387 elif sel_name == 'Practice' and self._practice_button is not None: 388 sel = self._practice_button 389 else: 390 sel = self._pay_with_tickets_button 391 bui.containerwidget(edit=self.root_widget, selected_child=sel) 392 393 def _update(self) -> None: 394 plus = bui.app.plus 395 assert plus is not None 396 397 # We may outlive our widgets. 398 if not self.root_widget: 399 return 400 401 # If we've been foregrounded/backgrounded we need to re-grab data. 402 if self._fg_state != bui.app.fg_state: 403 self._fg_state = bui.app.fg_state 404 self._have_valid_data = False 405 406 # If we need to run another tournament query, do so. 407 if not self._running_query and ( 408 (self._last_query_time is None) 409 or (not self._have_valid_data) 410 or (bui.apptime() - self._last_query_time > 30.0) 411 ): 412 plus.tournament_query( 413 args={ 414 'source': ( 415 'entry window' 416 if self._tournament_activity is None 417 else 'retry entry window' 418 ) 419 }, 420 callback=bui.WeakCall(self._on_tournament_query_response), 421 ) 422 self._last_query_time = bui.apptime() 423 self._running_query = True 424 425 # Grab the latest info on our tourney. 426 assert bui.app.classic is not None 427 self._tournament_info = bui.app.classic.accounts.tournament_info[ 428 self._tournament_id 429 ] 430 431 # If we don't have valid data always show a '-' for time. 432 if not self._have_valid_data: 433 bui.textwidget(edit=self._time_remaining_text, text='-') 434 else: 435 if self._seconds_remaining is not None: 436 self._seconds_remaining = max(0, self._seconds_remaining - 1) 437 bui.textwidget( 438 edit=self._time_remaining_text, 439 text=bui.timestring(self._seconds_remaining, centi=False), 440 ) 441 442 # Keep price up-to-date and update the button with it. 443 self._purchase_price = plus.get_v1_account_misc_read_val( 444 self._purchase_price_name, None 445 ) 446 447 bui.textwidget( 448 edit=self._ticket_cost_text, 449 text=( 450 bui.Lstr(resource='getTicketsWindow.freeText') 451 if self._purchase_price == 0 452 else bui.Lstr( 453 resource='getTicketsWindow.ticketsText', 454 subs=[ 455 ( 456 '${COUNT}', 457 ( 458 str(self._purchase_price) 459 if self._purchase_price is not None 460 else '?' 461 ), 462 ) 463 ], 464 ) 465 ), 466 position=( 467 self._ticket_cost_text_position_free 468 if self._purchase_price == 0 469 else self._ticket_cost_text_position 470 ), 471 scale=1.0 if self._purchase_price == 0 else 0.6, 472 ) 473 474 bui.textwidget( 475 edit=self._free_plays_remaining_text, 476 text=( 477 '' 478 if ( 479 self._tournament_info['freeTriesRemaining'] in [None, 0] 480 or self._purchase_price != 0 481 ) 482 else '' + str(self._tournament_info['freeTriesRemaining']) 483 ), 484 ) 485 486 bui.imagewidget( 487 edit=self._ticket_img, 488 opacity=0.2 if self._purchase_price == 0 else 1.0, 489 position=( 490 self._ticket_img_pos_free 491 if self._purchase_price == 0 492 else self._ticket_img_pos 493 ), 494 ) 495 496 if self._do_ad_btn: 497 enabled = plus.have_incentivized_ad() 498 have_ad_tries_remaining = ( 499 self._tournament_info['adTriesRemaining'] is not None 500 and self._tournament_info['adTriesRemaining'] > 0 501 ) 502 bui.textwidget( 503 edit=self._ad_text, 504 position=( 505 self._ad_text_position_remaining 506 if have_ad_tries_remaining 507 else self._ad_text_position 508 ), 509 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 510 ) 511 bui.imagewidget( 512 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 513 ) 514 bui.buttonwidget( 515 edit=self._pay_with_ad_btn, 516 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 517 ) 518 ad_plays_remaining_text = ( 519 '' 520 if not have_ad_tries_remaining 521 else '' + str(self._tournament_info['adTriesRemaining']) 522 ) 523 bui.textwidget( 524 edit=self._ad_plays_remaining_text, 525 text=ad_plays_remaining_text, 526 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 527 ) 528 529 try: 530 t_str = str(plus.get_v1_account_ticket_count()) 531 except Exception: 532 t_str = '?' 533 if self._get_tickets_button: 534 bui.buttonwidget( 535 edit=self._get_tickets_button, 536 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 537 ) 538 if self._ticket_count_text: 539 bui.textwidget( 540 edit=self._ticket_count_text, 541 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 542 ) 543 544 def _launch(self, practice: bool = False) -> None: 545 assert bui.app.classic is not None 546 if self._launched: 547 return 548 self._launched = True 549 launched = False 550 551 # If they gave us an existing, non-consistent 552 # practice activity, just restart it. 553 if ( 554 self._tournament_activity is not None 555 and not practice == self._tournament_activity.session.submit_score 556 ): 557 try: 558 if not practice: 559 bui.apptimer(0.1, bui.getsound('cashRegister').play) 560 bui.screenmessage( 561 bui.Lstr( 562 translate=( 563 'serverResponses', 564 'Entering tournament...', 565 ) 566 ), 567 color=(0, 1, 0), 568 ) 569 bui.apptimer(0 if practice else 0.3, self._transition_out) 570 launched = True 571 with self._tournament_activity.context: 572 self._tournament_activity.end( 573 {'outcome': 'restart'}, force=True 574 ) 575 576 # We can hit exceptions here if _tournament_activity ends before 577 # our restart attempt happens. 578 # In this case we'll fall back to launching a new session. 579 # This is not ideal since players will have to rejoin, etc., 580 # but it works for now. 581 except Exception: 582 logging.exception('Error restarting tournament activity.') 583 584 # If we had no existing activity (or were unable to restart it) 585 # launch a new session. 586 if not launched: 587 if not practice: 588 bui.apptimer(0.1, bui.getsound('cashRegister').play) 589 bui.screenmessage( 590 bui.Lstr( 591 translate=('serverResponses', 'Entering tournament...') 592 ), 593 color=(0, 1, 0), 594 ) 595 bui.apptimer( 596 0 if practice else 1.0, 597 lambda: ( 598 bui.app.classic.launch_coop_game( 599 self._tournament_info['game'], 600 args={ 601 'min_players': self._tournament_info['minPlayers'], 602 'max_players': self._tournament_info['maxPlayers'], 603 'tournament_id': self._tournament_id, 604 'submit_score': not practice, 605 }, 606 ) 607 if bui.app.classic is not None 608 else None 609 ), 610 ) 611 bui.apptimer(0 if practice else 1.25, self._transition_out) 612 613 def _on_pay_with_tickets_press(self) -> None: 614 # from bauiv1lib import gettickets 615 616 plus = bui.app.plus 617 assert plus is not None 618 619 # If we're already entering, ignore. 620 if self._entering: 621 return 622 623 if not self._have_valid_data: 624 bui.screenmessage( 625 bui.Lstr(resource='tournamentCheckingStateText'), 626 color=(1, 0, 0), 627 ) 628 bui.getsound('error').play() 629 return 630 631 # If we don't have a price. 632 if self._purchase_price is None: 633 bui.screenmessage( 634 bui.Lstr(resource='tournamentCheckingStateText'), 635 color=(1, 0, 0), 636 ) 637 bui.getsound('error').play() 638 return 639 640 # Deny if it looks like the tourney has ended. 641 if self._seconds_remaining == 0: 642 bui.screenmessage( 643 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 644 ) 645 bui.getsound('error').play() 646 return 647 648 # Deny if we don't have enough tickets. 649 ticket_count: int | None 650 try: 651 ticket_count = plus.get_v1_account_ticket_count() 652 except Exception: 653 # FIXME: should add a bui.NotSignedInError we can use here. 654 ticket_count = None 655 ticket_cost = self._purchase_price 656 if ticket_count is not None and ticket_count < ticket_cost: 657 # gettickets.show_get_tickets_prompt() 658 print('FIXME - show not-enough-tickets msg.') 659 bui.getsound('error').play() 660 self._transition_out() 661 return 662 663 cur_time = bui.apptime() 664 self._last_ticket_press_time = cur_time 665 assert isinstance(ticket_cost, int) 666 plus.in_game_purchase(self._purchase_name, ticket_cost) 667 668 self._entering = True 669 plus.add_v1_account_transaction( 670 { 671 'type': 'ENTER_TOURNAMENT', 672 'fee': self._fee, 673 'tournamentID': self._tournament_id, 674 } 675 ) 676 plus.run_v1_account_transactions() 677 self._launch() 678 679 def _on_pay_with_ad_press(self) -> None: 680 # If we're already entering, ignore. 681 if self._entering: 682 return 683 684 if not self._have_valid_data: 685 bui.screenmessage( 686 bui.Lstr(resource='tournamentCheckingStateText'), 687 color=(1, 0, 0), 688 ) 689 bui.getsound('error').play() 690 return 691 692 # Deny if it looks like the tourney has ended. 693 if self._seconds_remaining == 0: 694 bui.screenmessage( 695 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 696 ) 697 bui.getsound('error').play() 698 return 699 700 cur_time = bui.apptime() 701 if cur_time - self._last_ad_press_time > 5.0: 702 self._last_ad_press_time = cur_time 703 assert bui.app.classic is not None 704 bui.app.classic.ads.show_ad_2( 705 'tournament_entry', 706 on_completion_call=bui.WeakCall(self._on_ad_complete), 707 ) 708 709 def _on_practice_press(self) -> None: 710 plus = bui.app.plus 711 assert plus is not None 712 713 # If we're already entering, ignore. 714 if self._entering: 715 return 716 717 # Deny if it looks like the tourney has ended. 718 if self._seconds_remaining == 0: 719 bui.screenmessage( 720 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 721 ) 722 bui.getsound('error').play() 723 return 724 725 self._entering = True 726 self._launch(practice=True) 727 728 def _on_ad_complete(self, actually_showed: bool) -> None: 729 plus = bui.app.plus 730 assert plus is not None 731 732 # Make sure any transactions the ad added got locally applied 733 # (rewards added, etc.). 734 plus.run_v1_account_transactions() 735 736 # If we're already entering the tourney, ignore. 737 if self._entering: 738 return 739 740 if not actually_showed: 741 return 742 743 # This should have awarded us the tournament_entry_ad purchase; 744 # make sure that's present. 745 # (otherwise the server will ignore our tournament entry anyway) 746 if not plus.get_v1_account_product_purchased('tournament_entry_ad'): 747 print('no tournament_entry_ad purchase present in _on_ad_complete') 748 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 749 bui.getsound('error').play() 750 return 751 752 self._entering = True 753 plus.add_v1_account_transaction( 754 { 755 'type': 'ENTER_TOURNAMENT', 756 'fee': 'ad', 757 'tournamentID': self._tournament_id, 758 } 759 ) 760 plus.run_v1_account_transactions() 761 self._launch() 762 763 # def _on_get_tickets_press(self) -> None: 764 # from bauiv1lib import gettickets 765 766 # # If we're already entering, ignore presses. 767 # if self._entering: 768 # return 769 770 # # Bring up get-tickets window and then kill ourself (we're on the 771 # # overlay layer so we'd show up above it). 772 # gettickets.GetTicketsWindow( 773 # modal=True, origin_widget=self._get_tickets_button 774 # ) 775 # self._transition_out() 776 777 def _on_cancel(self) -> None: 778 plus = bui.app.plus 779 assert plus is not None 780 # Don't allow canceling for several seconds after poking an enter 781 # button if it looks like we're waiting on a purchase or entering 782 # the tournament. 783 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 784 plus.have_outstanding_v1_account_transactions() 785 or plus.get_v1_account_product_purchased(self._purchase_name) 786 or self._entering 787 ): 788 bui.getsound('error').play() 789 return 790 self._transition_out() 791 792 def _transition_out(self) -> None: 793 if not self.root_widget: 794 return 795 if not self._transitioning_out: 796 self._transitioning_out = True 797 self._save_state() 798 bui.containerwidget(edit=self.root_widget, transition='out_scale') 799 if self._on_close_call is not None: 800 self._on_close_call() 801 802 @override 803 def on_popup_cancel(self) -> None: 804 bui.getsound('swish').play() 805 self._on_cancel()
Popup window for entering tournaments.
TournamentEntryWindow( tournament_id: str, tournament_activity: bascenev1.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 # pylint: disable=too-many-positional-arguments 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_store_no_back', 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 278 self._seconds_remaining = None 279 280 bui.containerwidget( 281 edit=self.root_widget, cancel_button=self._cancel_button 282 ) 283 284 # Let's also ask the server for info about this tournament 285 # (time remaining, etc) so we can show the user time remaining, 286 # disallow entry if time has run out, etc. 287 # xoffs = 104 if bui.app.ui.use_toolbars else 0 288 self._time_remaining_text = bui.textwidget( 289 parent=self.root_widget, 290 position=(self._width / 2, 28), 291 size=(0, 0), 292 h_align='center', 293 v_align='center', 294 text='-', 295 scale=0.65, 296 maxwidth=100, 297 flatness=1.0, 298 color=(0.7, 0.7, 0.7), 299 ) 300 self._time_remaining_label_text = bui.textwidget( 301 parent=self.root_widget, 302 position=(self._width / 2, 45), 303 size=(0, 0), 304 h_align='center', 305 v_align='center', 306 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 307 scale=0.45, 308 flatness=1.0, 309 maxwidth=100, 310 color=(0.7, 0.7, 0.7), 311 ) 312 313 self._last_query_time: float | None = None 314 315 # If there seems to be a relatively-recent valid cached info for this 316 # tournament, use it. Otherwise we'll kick off a query ourselves. 317 if ( 318 self._tournament_id in bui.app.classic.accounts.tournament_info 319 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 320 'valid' 321 ] 322 and ( 323 bui.apptime() 324 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 325 'timeReceived' 326 ] 327 < 60 * 5 328 ) 329 ): 330 try: 331 info = bui.app.classic.accounts.tournament_info[ 332 self._tournament_id 333 ] 334 self._seconds_remaining = max( 335 0, 336 info['timeRemaining'] 337 - int((bui.apptime() - info['timeReceived'])), 338 ) 339 self._have_valid_data = True 340 self._last_query_time = bui.apptime() 341 except Exception: 342 logging.exception('Error using valid tourney data.') 343 self._have_valid_data = False 344 else: 345 self._have_valid_data = False 346 347 self._fg_state = bui.app.fg_state 348 self._running_query = False 349 self._update_timer = bui.AppTimer( 350 1.0, bui.WeakCall(self._update), repeat=True 351 ) 352 self._update() 353 self._restore_state()
@override
def
on_popup_cancel(self) -> None:
802 @override 803 def on_popup_cancel(self) -> None: 804 bui.getsound('swish').play() 805 self._on_cancel()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.