bauiv1lib.tournamententry

Defines a popup window for entering tournaments.

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

Popup window for entering tournaments.

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

Called when the popup is canceled.

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