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