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()
class TournamentEntryWindow(bauiv1lib.popup.PopupWindow):
 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.