bauiv1lib.tournamententry

Defines a popup window for entering tournaments.

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

Popup window for entering tournaments.

TournamentEntryWindow( tournament_id: str, tournament_activity: bascenev1._activity.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), on_close_call: Optional[Callable[[], Any]] = None)
 22    def __init__(
 23        self,
 24        tournament_id: str,
 25        tournament_activity: bs.Activity | None = None,
 26        position: tuple[float, float] = (0.0, 0.0),
 27        delegate: Any = None,
 28        scale: float | None = None,
 29        offset: tuple[float, float] = (0.0, 0.0),
 30        on_close_call: Callable[[], Any] | None = None,
 31    ):
 32        # Needs some tidying.
 33        # pylint: disable=too-many-locals
 34        # pylint: disable=too-many-branches
 35        # pylint: disable=too-many-statements
 36
 37        assert bui.app.classic is not None
 38        assert bui.app.plus
 39        bui.set_analytics_screen('Tournament Entry Window')
 40
 41        self._tournament_id = tournament_id
 42        self._tournament_info = bui.app.classic.accounts.tournament_info[
 43            self._tournament_id
 44        ]
 45
 46        # Set a few vars depending on the tourney fee.
 47        self._fee = self._tournament_info['fee']
 48        self._allow_ads = self._tournament_info['allowAds']
 49        if self._fee == 4:
 50            self._purchase_name = 'tournament_entry_4'
 51            self._purchase_price_name = 'price.tournament_entry_4'
 52        elif self._fee == 3:
 53            self._purchase_name = 'tournament_entry_3'
 54            self._purchase_price_name = 'price.tournament_entry_3'
 55        elif self._fee == 2:
 56            self._purchase_name = 'tournament_entry_2'
 57            self._purchase_price_name = 'price.tournament_entry_2'
 58        elif self._fee == 1:
 59            self._purchase_name = 'tournament_entry_1'
 60            self._purchase_price_name = 'price.tournament_entry_1'
 61        else:
 62            if self._fee != 0:
 63                raise ValueError('invalid fee: ' + str(self._fee))
 64            self._purchase_name = 'tournament_entry_0'
 65            self._purchase_price_name = 'price.tournament_entry_0'
 66
 67        self._purchase_price: int | None = None
 68
 69        self._on_close_call = on_close_call
 70        if scale is None:
 71            uiscale = bui.app.ui_v1.uiscale
 72            scale = (
 73                2.3
 74                if uiscale is bui.UIScale.SMALL
 75                else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
 76            )
 77        self._delegate = delegate
 78        self._transitioning_out = False
 79
 80        self._tournament_activity = tournament_activity
 81
 82        self._width: float = 340.0
 83        self._height: float = 225.0
 84
 85        bg_color = (0.5, 0.4, 0.6)
 86
 87        # Show the practice button as long as we're not
 88        # restarting while on a paid tournament run.
 89        self._do_practice = (
 90            self._tournament_activity is None
 91            and bui.app.config.get('tournament_practice_enabled', False)
 92        )
 93
 94        off_p = 0 if not self._do_practice else 48
 95        self._height += off_p * 0.933
 96
 97        # Creates our root_widget.
 98        super().__init__(
 99            position=position,
100            size=(self._width, self._height),
101            scale=scale,
102            bg_color=bg_color,
103            offset=offset,
104            toolbar_visibility='menu_currency',
105        )
106
107        self._last_ad_press_time = -9999.0
108        self._last_ticket_press_time = -9999.0
109        self._entering = False
110        self._launched = False
111
112        # Show the ad button only if we support ads *and* it has a level 1 fee.
113        self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads
114
115        x_offs = 0 if self._do_ad_btn else 85
116
117        self._cancel_button = bui.buttonwidget(
118            parent=self.root_widget,
119            position=(40, self._height - 34),
120            size=(60, 60),
121            scale=0.5,
122            label='',
123            color=bg_color,
124            on_activate_call=self._on_cancel,
125            autoselect=True,
126            icon=bui.gettexture('crossOut'),
127            iconscale=1.2,
128        )
129
130        self._title_text = bui.textwidget(
131            parent=self.root_widget,
132            position=(self._width * 0.5, self._height - 20),
133            size=(0, 0),
134            h_align='center',
135            v_align='center',
136            scale=0.6,
137            text=bui.Lstr(resource='tournamentEntryText'),
138            maxwidth=180,
139            color=(1, 1, 1, 0.4),
140        )
141
142        btn = self._pay_with_tickets_button = bui.buttonwidget(
143            parent=self.root_widget,
144            position=(30 + x_offs, 60 + off_p),
145            autoselect=True,
146            button_type='square',
147            size=(120, 120),
148            label='',
149            on_activate_call=self._on_pay_with_tickets_press,
150        )
151        self._ticket_img_pos = (50 + x_offs, 94 + off_p)
152        self._ticket_img_pos_free = (50 + x_offs, 80 + off_p)
153        self._ticket_img = bui.imagewidget(
154            parent=self.root_widget,
155            draw_controller=btn,
156            size=(80, 80),
157            position=self._ticket_img_pos,
158            texture=bui.gettexture('tickets'),
159        )
160        self._ticket_cost_text_position = (87 + x_offs, 88 + off_p)
161        self._ticket_cost_text_position_free = (87 + x_offs, 120 + off_p)
162        self._ticket_cost_text = bui.textwidget(
163            parent=self.root_widget,
164            draw_controller=btn,
165            position=self._ticket_cost_text_position,
166            size=(0, 0),
167            h_align='center',
168            v_align='center',
169            scale=0.6,
170            text='',
171            maxwidth=95,
172            color=(0, 1, 0),
173        )
174        self._free_plays_remaining_text = bui.textwidget(
175            parent=self.root_widget,
176            draw_controller=btn,
177            position=(87 + x_offs, 78 + off_p),
178            size=(0, 0),
179            h_align='center',
180            v_align='center',
181            scale=0.33,
182            text='',
183            maxwidth=95,
184            color=(0, 0.8, 0),
185        )
186        self._pay_with_ad_btn: bui.Widget | None
187        if self._do_ad_btn:
188            btn = self._pay_with_ad_btn = bui.buttonwidget(
189                parent=self.root_widget,
190                position=(190, 60 + off_p),
191                autoselect=True,
192                button_type='square',
193                size=(120, 120),
194                label='',
195                on_activate_call=self._on_pay_with_ad_press,
196            )
197            self._pay_with_ad_img = bui.imagewidget(
198                parent=self.root_widget,
199                draw_controller=btn,
200                size=(80, 80),
201                position=(210, 94 + off_p),
202                texture=bui.gettexture('tv'),
203            )
204
205            self._ad_text_position = (251, 88 + off_p)
206            self._ad_text_position_remaining = (251, 92 + off_p)
207            have_ad_tries_remaining = (
208                self._tournament_info['adTriesRemaining'] is not None
209            )
210            self._ad_text = bui.textwidget(
211                parent=self.root_widget,
212                draw_controller=btn,
213                position=(
214                    self._ad_text_position_remaining
215                    if have_ad_tries_remaining
216                    else self._ad_text_position
217                ),
218                size=(0, 0),
219                h_align='center',
220                v_align='center',
221                scale=0.6,
222                # Note: AdMob now requires rewarded ad usage
223                # specifically says 'Ad' in it.
224                text=bui.Lstr(resource='watchAnAdText'),
225                maxwidth=95,
226                color=(0, 1, 0),
227            )
228            ad_plays_remaining_text = (
229                ''
230                if not have_ad_tries_remaining
231                else '' + str(self._tournament_info['adTriesRemaining'])
232            )
233            self._ad_plays_remaining_text = bui.textwidget(
234                parent=self.root_widget,
235                draw_controller=btn,
236                position=(251, 78 + off_p),
237                size=(0, 0),
238                h_align='center',
239                v_align='center',
240                scale=0.33,
241                text=ad_plays_remaining_text,
242                maxwidth=95,
243                color=(0, 0.8, 0),
244            )
245
246            bui.textwidget(
247                parent=self.root_widget,
248                position=(self._width * 0.5, 120 + off_p),
249                size=(0, 0),
250                h_align='center',
251                v_align='center',
252                scale=0.6,
253                text=bui.Lstr(
254                    resource='orText', subs=[('${A}', ''), ('${B}', '')]
255                ),
256                maxwidth=35,
257                color=(1, 1, 1, 0.5),
258            )
259        else:
260            self._pay_with_ad_btn = None
261
262        btn_size = (150, 45)
263        btn_pos = (self._width / 2 - btn_size[0] / 2, self._width / 2 - 110)
264        self._practice_button = None
265        if self._do_practice:
266            self._practice_button = bui.buttonwidget(
267                parent=self.root_widget,
268                position=btn_pos,
269                autoselect=True,
270                size=btn_size,
271                label=bui.Lstr(resource='practiceText'),
272                on_activate_call=self._on_practice_press,
273            )
274
275        self._get_tickets_button: bui.Widget | None = None
276        self._ticket_count_text: bui.Widget | None = None
277        if not bui.app.ui_v1.use_toolbars:
278            if bui.app.classic.allow_ticket_purchases:
279                self._get_tickets_button = bui.buttonwidget(
280                    parent=self.root_widget,
281                    position=(self._width - 190 + 105, self._height - 34),
282                    autoselect=True,
283                    scale=0.5,
284                    size=(120, 60),
285                    textcolor=(0.2, 1, 0.2),
286                    label=bui.charstr(bui.SpecialChar.TICKET),
287                    color=(0.65, 0.5, 0.8),
288                    on_activate_call=self._on_get_tickets_press,
289                )
290            else:
291                self._ticket_count_text = bui.textwidget(
292                    parent=self.root_widget,
293                    scale=0.5,
294                    position=(self._width - 190 + 125, self._height - 34),
295                    color=(0.2, 1, 0.2),
296                    h_align='center',
297                    v_align='center',
298                )
299
300        self._seconds_remaining = None
301
302        bui.containerwidget(
303            edit=self.root_widget, cancel_button=self._cancel_button
304        )
305
306        # Let's also ask the server for info about this tournament
307        # (time remaining, etc) so we can show the user time remaining,
308        # disallow entry if time has run out, etc.
309        # xoffs = 104 if bui.app.ui.use_toolbars else 0
310        self._time_remaining_text = bui.textwidget(
311            parent=self.root_widget,
312            position=(self._width / 2, 28),
313            size=(0, 0),
314            h_align='center',
315            v_align='center',
316            text='-',
317            scale=0.65,
318            maxwidth=100,
319            flatness=1.0,
320            color=(0.7, 0.7, 0.7),
321        )
322        self._time_remaining_label_text = bui.textwidget(
323            parent=self.root_widget,
324            position=(self._width / 2, 45),
325            size=(0, 0),
326            h_align='center',
327            v_align='center',
328            text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'),
329            scale=0.45,
330            flatness=1.0,
331            maxwidth=100,
332            color=(0.7, 0.7, 0.7),
333        )
334
335        self._last_query_time: float | None = None
336
337        # If there seems to be a relatively-recent valid cached info for this
338        # tournament, use it. Otherwise we'll kick off a query ourselves.
339        if (
340            self._tournament_id in bui.app.classic.accounts.tournament_info
341            and bui.app.classic.accounts.tournament_info[self._tournament_id][
342                'valid'
343            ]
344            and (
345                bui.apptime()
346                - bui.app.classic.accounts.tournament_info[self._tournament_id][
347                    'timeReceived'
348                ]
349                < 60 * 5
350            )
351        ):
352            try:
353                info = bui.app.classic.accounts.tournament_info[
354                    self._tournament_id
355                ]
356                self._seconds_remaining = max(
357                    0,
358                    info['timeRemaining']
359                    - int((bui.apptime() - info['timeReceived'])),
360                )
361                self._have_valid_data = True
362                self._last_query_time = bui.apptime()
363            except Exception:
364                logging.exception('Error using valid tourney data.')
365                self._have_valid_data = False
366        else:
367            self._have_valid_data = False
368
369        self._fg_state = bui.app.fg_state
370        self._running_query = False
371        self._update_timer = bui.AppTimer(
372            1.0, bui.WeakCall(self._update), repeat=True
373        )
374        self._update()
375        self._restore_state()
@override
def on_popup_cancel(self) -> None:
823    @override
824    def on_popup_cancel(self) -> None:
825        bui.getsound('swish').play()
826        self._on_cancel()

Called when the popup is canceled.

Cancels can occur due to clicking outside the window, hitting escape, etc.