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

Called when the popup is canceled.

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