Source code for bauiv1lib.tournamententry

# Released under the MIT License. See LICENSE for details.
#
"""Defines a popup window for entering tournaments."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, override

from bauiv1lib.popup import PopupWindow
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any, Callable
    import bascenev1 as bs


[docs] class TournamentEntryWindow(PopupWindow): """Popup window for entering tournaments.""" def __init__( self, tournament_id: str, tournament_activity: bs.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: Callable[[], Any] | None = None, ): # pylint: disable=too-many-positional-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements assert bui.app.classic is not None assert bui.app.plus bui.set_analytics_screen('Tournament Entry Window') self._tournament_id = tournament_id self._tournament_info = bui.app.classic.accounts.tournament_info[ self._tournament_id ] # Set a few vars depending on the tourney fee. self._fee = self._tournament_info['fee'] self._allow_ads = self._tournament_info['allowAds'] if self._fee == 4: self._purchase_name = 'tournament_entry_4' self._purchase_price_name = 'price.tournament_entry_4' elif self._fee == 3: self._purchase_name = 'tournament_entry_3' self._purchase_price_name = 'price.tournament_entry_3' elif self._fee == 2: self._purchase_name = 'tournament_entry_2' self._purchase_price_name = 'price.tournament_entry_2' elif self._fee == 1: self._purchase_name = 'tournament_entry_1' self._purchase_price_name = 'price.tournament_entry_1' else: if self._fee != 0: raise ValueError('invalid fee: ' + str(self._fee)) self._purchase_name = 'tournament_entry_0' self._purchase_price_name = 'price.tournament_entry_0' self._purchase_price: int | None = None self._on_close_call = on_close_call if scale is None: uiscale = bui.app.ui_v1.uiscale scale = ( 2.3 if uiscale is bui.UIScale.SMALL else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23 ) self._delegate = delegate self._transitioning_out = False self._tournament_activity = tournament_activity self._width: float = 340.0 self._height: float = 225.0 bg_color = (0.5, 0.4, 0.6) # Show the practice button as long as we're not # restarting while on a paid tournament run. self._do_practice = ( self._tournament_activity is None and bui.app.config.get('tournament_practice_enabled', False) ) off_p = 0 if not self._do_practice else 48 self._height += off_p * 0.933 # Creates our root_widget. super().__init__( position=position, size=(self._width, self._height), scale=scale, bg_color=bg_color, offset=offset, toolbar_visibility='menu_store_no_back', ) self._last_ad_press_time = -9999.0 self._last_ticket_press_time = -9999.0 self._entering = False self._launched = False # Show the ad button only if we support ads *and* it has a level 1 fee. self._do_ad_btn = bui.app.plus.has_video_ads() and self._allow_ads x_offs = 0 if self._do_ad_btn else 85 self._cancel_button = bui.buttonwidget( parent=self.root_widget, position=(40, self._height - 34), size=(60, 60), scale=0.5, label='', color=bg_color, on_activate_call=self._on_cancel, autoselect=True, icon=bui.gettexture('crossOut'), iconscale=1.2, ) self._title_text = bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, self._height - 20), size=(0, 0), h_align='center', v_align='center', scale=0.6, text=bui.Lstr(resource='tournamentEntryText'), maxwidth=180, color=(1, 1, 1, 0.4), ) btn = self._pay_with_tickets_button = bui.buttonwidget( parent=self.root_widget, position=(30 + x_offs, 60 + off_p), autoselect=True, button_type='square', size=(120, 120), label='', on_activate_call=self._on_pay_with_tickets_press, ) self._ticket_img_pos = (50 + x_offs, 94 + off_p) self._ticket_img_pos_free = (50 + x_offs, 80 + off_p) self._ticket_img = bui.imagewidget( parent=self.root_widget, draw_controller=btn, size=(80, 80), position=self._ticket_img_pos, texture=bui.gettexture('tickets'), ) self._ticket_cost_text_position = (87 + x_offs, 88 + off_p) self._ticket_cost_text_position_free = (87 + x_offs, 120 + off_p) self._ticket_cost_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=self._ticket_cost_text_position, size=(0, 0), h_align='center', v_align='center', scale=0.6, text='', maxwidth=95, color=(0, 1, 0), ) self._free_plays_remaining_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=(87 + x_offs, 78 + off_p), size=(0, 0), h_align='center', v_align='center', scale=0.33, text='', maxwidth=95, color=(0, 0.8, 0), ) self._pay_with_ad_btn: bui.Widget | None if self._do_ad_btn: btn = self._pay_with_ad_btn = bui.buttonwidget( parent=self.root_widget, position=(190, 60 + off_p), autoselect=True, button_type='square', size=(120, 120), label='', on_activate_call=self._on_pay_with_ad_press, ) self._pay_with_ad_img = bui.imagewidget( parent=self.root_widget, draw_controller=btn, size=(80, 80), position=(210, 94 + off_p), texture=bui.gettexture('tv'), ) self._ad_text_position = (251, 88 + off_p) self._ad_text_position_remaining = (251, 92 + off_p) have_ad_tries_remaining = ( self._tournament_info['adTriesRemaining'] is not None ) self._ad_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=( self._ad_text_position_remaining if have_ad_tries_remaining else self._ad_text_position ), size=(0, 0), h_align='center', v_align='center', scale=0.6, # Note: AdMob now requires rewarded ad usage # specifically says 'Ad' in it. text=bui.Lstr(resource='watchAnAdText'), maxwidth=95, color=(0, 1, 0), ) ad_plays_remaining_text = ( '' if not have_ad_tries_remaining else '' + str(self._tournament_info['adTriesRemaining']) ) self._ad_plays_remaining_text = bui.textwidget( parent=self.root_widget, draw_controller=btn, position=(251, 78 + off_p), size=(0, 0), h_align='center', v_align='center', scale=0.33, text=ad_plays_remaining_text, maxwidth=95, color=(0, 0.8, 0), ) bui.textwidget( parent=self.root_widget, position=(self._width * 0.5, 120 + off_p), size=(0, 0), h_align='center', v_align='center', scale=0.6, text=bui.Lstr( resource='orText', subs=[('${A}', ''), ('${B}', '')] ), maxwidth=35, color=(1, 1, 1, 0.5), ) else: self._pay_with_ad_btn = None btn_size = (150, 45) btn_pos = (self._width / 2 - btn_size[0] / 2, self._width / 2 - 110) self._practice_button = None if self._do_practice: self._practice_button = bui.buttonwidget( parent=self.root_widget, position=btn_pos, autoselect=True, size=btn_size, label=bui.Lstr(resource='practiceText'), on_activate_call=self._on_practice_press, ) self._get_tickets_button: bui.Widget | None = None self._ticket_count_text: bui.Widget | None = None self._seconds_remaining = None bui.containerwidget( edit=self.root_widget, cancel_button=self._cancel_button ) # Let's also ask the server for info about this tournament # (time remaining, etc) so we can show the user time remaining, # disallow entry if time has run out, etc. # xoffs = 104 if bui.app.ui.use_toolbars else 0 self._time_remaining_text = bui.textwidget( parent=self.root_widget, position=(self._width / 2, 28), size=(0, 0), h_align='center', v_align='center', text='-', scale=0.65, maxwidth=100, flatness=1.0, color=(0.7, 0.7, 0.7), ) self._time_remaining_label_text = bui.textwidget( parent=self.root_widget, position=(self._width / 2, 45), size=(0, 0), h_align='center', v_align='center', text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), scale=0.45, flatness=1.0, maxwidth=100, color=(0.7, 0.7, 0.7), ) self._last_query_time: float | None = None # If there seems to be a relatively-recent valid cached info for this # tournament, use it. Otherwise we'll kick off a query ourselves. if ( self._tournament_id in bui.app.classic.accounts.tournament_info and bui.app.classic.accounts.tournament_info[self._tournament_id][ 'valid' ] and ( bui.apptime() - bui.app.classic.accounts.tournament_info[self._tournament_id][ 'timeReceived' ] < 60 * 5 ) ): try: info = bui.app.classic.accounts.tournament_info[ self._tournament_id ] self._seconds_remaining = max( 0, info['timeRemaining'] - int((bui.apptime() - info['timeReceived'])), ) self._have_valid_data = True self._last_query_time = bui.apptime() except Exception: logging.exception('Error using valid tourney data.') self._have_valid_data = False else: self._have_valid_data = False self._fg_state = bui.app.fg_state self._running_query = False self._update_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update), repeat=True ) self._update() self._restore_state() def _on_tournament_query_response( self, data: dict[str, Any] | None ) -> None: assert bui.app.classic is not None accounts = bui.app.classic.accounts self._running_query = False if data is not None: data = data['t'] # This used to be the whole payload. accounts.cache_tournament_info(data) self._seconds_remaining = accounts.tournament_info[ self._tournament_id ]['timeRemaining'] self._have_valid_data = True def _save_state(self) -> None: if not self.root_widget: return sel = self.root_widget.get_selected_child() if sel == self._pay_with_ad_btn: sel_name = 'Ad' elif sel == self._practice_button: sel_name = 'Practice' else: sel_name = 'Tickets' cfg = bui.app.config cfg['Tournament Pay Selection'] = sel_name cfg.commit() def _restore_state(self) -> None: sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') if sel_name == 'Ad' and self._pay_with_ad_btn is not None: sel = self._pay_with_ad_btn elif sel_name == 'Practice' and self._practice_button is not None: sel = self._practice_button else: sel = self._pay_with_tickets_button bui.containerwidget(edit=self.root_widget, selected_child=sel) def _update(self) -> None: plus = bui.app.plus assert plus is not None # We may outlive our widgets. if not self.root_widget: return # If we've been foregrounded/backgrounded we need to re-grab data. if self._fg_state != bui.app.fg_state: self._fg_state = bui.app.fg_state self._have_valid_data = False # If we need to run another tournament query, do so. if not self._running_query and ( (self._last_query_time is None) or (not self._have_valid_data) or (bui.apptime() - self._last_query_time > 30.0) ): plus.tournament_query( args={ 'source': ( 'entry window' if self._tournament_activity is None else 'retry entry window' ) }, callback=bui.WeakCall(self._on_tournament_query_response), ) self._last_query_time = bui.apptime() self._running_query = True # Grab the latest info on our tourney. assert bui.app.classic is not None self._tournament_info = bui.app.classic.accounts.tournament_info[ self._tournament_id ] # If we don't have valid data always show a '-' for time. if not self._have_valid_data: bui.textwidget(edit=self._time_remaining_text, text='-') else: if self._seconds_remaining is not None: self._seconds_remaining = max(0, self._seconds_remaining - 1) bui.textwidget( edit=self._time_remaining_text, text=bui.timestring(self._seconds_remaining, centi=False), ) # Keep price up-to-date and update the button with it. self._purchase_price = plus.get_v1_account_misc_read_val( self._purchase_price_name, None ) bui.textwidget( edit=self._ticket_cost_text, text=( bui.Lstr(resource='getTicketsWindow.freeText') if self._purchase_price == 0 else bui.Lstr( resource='getTicketsWindow.ticketsText', subs=[ ( '${COUNT}', ( str(self._purchase_price) if self._purchase_price is not None else '?' ), ) ], ) ), position=( self._ticket_cost_text_position_free if self._purchase_price == 0 else self._ticket_cost_text_position ), scale=1.0 if self._purchase_price == 0 else 0.6, ) bui.textwidget( edit=self._free_plays_remaining_text, text=( '' if ( self._tournament_info['freeTriesRemaining'] in [None, 0] or self._purchase_price != 0 ) else '' + str(self._tournament_info['freeTriesRemaining']) ), ) bui.imagewidget( edit=self._ticket_img, opacity=0.2 if self._purchase_price == 0 else 1.0, position=( self._ticket_img_pos_free if self._purchase_price == 0 else self._ticket_img_pos ), ) if self._do_ad_btn: enabled = plus.have_incentivized_ad() have_ad_tries_remaining = ( self._tournament_info['adTriesRemaining'] is not None and self._tournament_info['adTriesRemaining'] > 0 ) bui.textwidget( edit=self._ad_text, position=( self._ad_text_position_remaining if have_ad_tries_remaining else self._ad_text_position ), color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), ) bui.imagewidget( edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 ) bui.buttonwidget( edit=self._pay_with_ad_btn, color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), ) ad_plays_remaining_text = ( '' if not have_ad_tries_remaining else '' + str(self._tournament_info['adTriesRemaining']) ) bui.textwidget( edit=self._ad_plays_remaining_text, text=ad_plays_remaining_text, color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), ) try: t_str = str(plus.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: bui.buttonwidget( edit=self._get_tickets_button, label=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) if self._ticket_count_text: bui.textwidget( edit=self._ticket_count_text, text=bui.charstr(bui.SpecialChar.TICKET) + t_str, ) def _launch(self, practice: bool = False) -> None: assert bui.app.classic is not None if self._launched: return self._launched = True launched = False # If they gave us an existing, non-consistent # practice activity, just restart it. if ( self._tournament_activity is not None and not practice == self._tournament_activity.session.submit_score ): try: if not practice: bui.apptimer(0.1, bui.getsound('cashRegister').play) bui.screenmessage( bui.Lstr( translate=( 'serverResponses', 'Entering tournament...', ) ), color=(0, 1, 0), ) bui.apptimer(0 if practice else 0.3, self._transition_out) launched = True with self._tournament_activity.context: self._tournament_activity.end( {'outcome': 'restart'}, force=True ) # We can hit exceptions here if _tournament_activity ends before # our restart attempt happens. # In this case we'll fall back to launching a new session. # This is not ideal since players will have to rejoin, etc., # but it works for now. except Exception: logging.exception('Error restarting tournament activity.') # If we had no existing activity (or were unable to restart it) # launch a new session. if not launched: if not practice: bui.apptimer(0.1, bui.getsound('cashRegister').play) bui.screenmessage( bui.Lstr( translate=('serverResponses', 'Entering tournament...') ), color=(0, 1, 0), ) bui.apptimer( 0 if practice else 1.0, lambda: ( bui.app.classic.launch_coop_game( self._tournament_info['game'], args={ 'min_players': self._tournament_info['minPlayers'], 'max_players': self._tournament_info['maxPlayers'], 'tournament_id': self._tournament_id, 'submit_score': not practice, }, ) if bui.app.classic is not None else None ), ) bui.apptimer(0 if practice else 1.25, self._transition_out) def _on_pay_with_tickets_press(self) -> None: # from bauiv1lib import gettickets plus = bui.app.plus assert plus is not None # If we're already entering, ignore. if self._entering: return if not self._have_valid_data: bui.screenmessage( bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0), ) bui.getsound('error').play() return # If we don't have a price. if self._purchase_price is None: bui.screenmessage( bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0), ) bui.getsound('error').play() return # Deny if it looks like the tourney has ended. if self._seconds_remaining == 0: bui.screenmessage( bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) bui.getsound('error').play() return # Deny if we don't have enough tickets. ticket_count: int | None try: ticket_count = plus.get_v1_account_ticket_count() except Exception: # FIXME: should add a bui.NotSignedInError we can use here. ticket_count = None ticket_cost = self._purchase_price if ticket_count is not None and ticket_count < ticket_cost: # gettickets.show_get_tickets_prompt() print('FIXME - show not-enough-tickets msg.') bui.getsound('error').play() self._transition_out() return cur_time = bui.apptime() self._last_ticket_press_time = cur_time assert isinstance(ticket_cost, int) plus.in_game_purchase(self._purchase_name, ticket_cost) self._entering = True plus.add_v1_account_transaction( { 'type': 'ENTER_TOURNAMENT', 'fee': self._fee, 'tournamentID': self._tournament_id, } ) plus.run_v1_account_transactions() self._launch() def _on_pay_with_ad_press(self) -> None: # If we're already entering, ignore. if self._entering: return if not self._have_valid_data: bui.screenmessage( bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0), ) bui.getsound('error').play() return # Deny if it looks like the tourney has ended. if self._seconds_remaining == 0: bui.screenmessage( bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) bui.getsound('error').play() return cur_time = bui.apptime() if cur_time - self._last_ad_press_time > 5.0: self._last_ad_press_time = cur_time assert bui.app.classic is not None bui.app.classic.ads.show_ad_2( 'tournament_entry', on_completion_call=bui.WeakCall(self._on_ad_complete), ) def _on_practice_press(self) -> None: plus = bui.app.plus assert plus is not None # If we're already entering, ignore. if self._entering: return # Deny if it looks like the tourney has ended. if self._seconds_remaining == 0: bui.screenmessage( bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) bui.getsound('error').play() return self._entering = True self._launch(practice=True) def _on_ad_complete(self, actually_showed: bool) -> None: plus = bui.app.plus assert plus is not None # Make sure any transactions the ad added got locally applied # (rewards added, etc.). plus.run_v1_account_transactions() # If we're already entering the tourney, ignore. if self._entering: return if not actually_showed: return # This should have awarded us the tournament_entry_ad purchase; # make sure that's present. # (otherwise the server will ignore our tournament entry anyway) if not plus.get_v1_account_product_purchased('tournament_entry_ad'): print('no tournament_entry_ad purchase present in _on_ad_complete') bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) bui.getsound('error').play() return self._entering = True plus.add_v1_account_transaction( { 'type': 'ENTER_TOURNAMENT', 'fee': 'ad', 'tournamentID': self._tournament_id, } ) plus.run_v1_account_transactions() self._launch() # def _on_get_tickets_press(self) -> None: # from bauiv1lib import gettickets # # If we're already entering, ignore presses. # if self._entering: # return # # Bring up get-tickets window and then kill ourself (we're on the # # overlay layer so we'd show up above it). # gettickets.GetTicketsWindow( # modal=True, origin_widget=self._get_tickets_button # ) # self._transition_out() def _on_cancel(self) -> None: plus = bui.app.plus assert plus is not None # Don't allow canceling for several seconds after poking an enter # button if it looks like we're waiting on a purchase or entering # the tournament. if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( plus.have_outstanding_v1_account_transactions() or plus.get_v1_account_product_purchased(self._purchase_name) or self._entering ): bui.getsound('error').play() return self._transition_out() def _transition_out(self) -> None: if not self.root_widget: return if not self._transitioning_out: self._transitioning_out = True self._save_state() bui.containerwidget(edit=self.root_widget, transition='out_scale') if self._on_close_call is not None: self._on_close_call()
[docs] @override def on_popup_cancel(self) -> None: bui.getsound('swish').play() self._on_cancel()