From bb7916d3c42a0c4a3cb184789117b4e1a48bbaee Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Tue, 24 Aug 2021 23:18:53 +0000 Subject: [PATCH] Move logic for calculating play statistics into common module and backend base module. --- bemani/backend/base.py | 122 +++++++++-- bemani/backend/ddr/common.py | 14 +- bemani/backend/ddr/ddr2013.py | 12 +- bemani/backend/ddr/ddr2014.py | 12 +- bemani/backend/ddr/ddrx3.py | 12 +- bemani/backend/museca/museca1.py | 18 +- bemani/backend/museca/museca1plus.py | 18 +- bemani/backend/popn/eclale.py | 20 +- bemani/backend/popn/fantasia.py | 18 +- bemani/backend/popn/lapistoria.py | 20 +- bemani/backend/popn/sunnypark.py | 18 +- bemani/backend/popn/usaneko.py | 66 ++++-- bemani/backend/reflec/colette.py | 18 +- bemani/backend/reflec/groovin.py | 18 +- bemani/backend/reflec/limelight.py | 19 +- bemani/backend/reflec/reflecbeat.py | 17 +- bemani/backend/reflec/volzza.py | 18 +- bemani/backend/reflec/volzza2.py | 18 +- bemani/backend/sdvx/gravitywars_s1.py | 18 +- bemani/backend/sdvx/gravitywars_s2.py | 18 +- bemani/backend/sdvx/heavenlyhaven.py | 18 +- bemani/backend/sdvx/infiniteinfection.py | 18 +- bemani/common/__init__.py | 3 +- bemani/common/validateddict.py | 34 +++ bemani/data/mysql/api.py | 4 +- bemani/data/mysql/network.py | 2 +- bemani/tests/test_PlayStats.py | 260 +++++++++++++++++++++++ 27 files changed, 520 insertions(+), 313 deletions(-) create mode 100644 bemani/tests/test_PlayStats.py diff --git a/bemani/backend/base.py b/bemani/backend/base.py index c7bb9f6..a034f35 100644 --- a/bemani/backend/base.py +++ b/bemani/backend/base.py @@ -2,7 +2,7 @@ from abc import ABC import traceback from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type -from bemani.common import Model, ValidatedDict, Profile, GameConstants, Time +from bemani.common import Model, ValidatedDict, Profile, PlayStatistics, GameConstants, Time from bemani.data import Config, Data, UserID, RemoteUser @@ -318,7 +318,7 @@ class Base(ABC): raise Exception('Trying to save a remote profile locally!') self.data.local.user.put_profile(self.game, self.version, userid, profile) - def update_play_statistics(self, userid: UserID, extra_stats: Optional[Dict[str, Any]]=None) -> None: + def update_play_statistics(self, userid: UserID, stats: Optional[PlayStatistics] = None) -> None: """ Given a user ID, calculate new play statistics. @@ -327,18 +327,19 @@ class Base(ABC): Parameters: userid - The user ID we are binding the profile for. + stats - A play statistics object we should store extra data from. """ if RemoteUser.is_remote(userid): raise Exception('Trying to save remote statistics locally!') # We store the play statistics in a series-wide settings blob so its available # across all game versions, since it isn't game-specific. - settings = self.get_play_statistics(userid) + settings = self.data.local.game.get_settings(self.game, userid) or ValidatedDict({}) - if extra_stats is not None: - for key in extra_stats: + if stats is not None: + for key in stats: # Make sure we don't override anything we manage here - if key in [ + if key in { 'total_plays', 'today_plays', 'total_days', @@ -346,14 +347,14 @@ class Base(ABC): 'last_play_timestamp', 'last_play_date', 'consecutive_days', - ]: + }: continue # Safe to copy over - settings[key] = extra_stats[key] + settings[key] = stats[key] settings.replace_int('total_plays', settings.get_int('total_plays') + 1) - settings.replace_int('first_play_timestamp', settings.get_int('first_play_timestamp', int(Time.now()))) - settings.replace_int('last_play_timestamp', int(Time.now())) + settings.replace_int('first_play_timestamp', settings.get_int('first_play_timestamp', Time.now())) + settings.replace_int('last_play_timestamp', Time.now()) last_play_date = settings.get_int_array('last_play_date', 3) today_play_date = Time.todays_date() @@ -363,13 +364,13 @@ class Base(ABC): last_play_date[1] == today_play_date[1] and last_play_date[2] == today_play_date[2] ): - # We already played today, add one + # We already played today, add one. settings.replace_int('today_plays', settings.get_int('today_plays') + 1) else: - # We played on a new day, so count total days up + # We played on a new day, so count total days up. settings.replace_int('total_days', settings.get_int('total_days') + 1) - # We haven't played yet today, reset to one + # We played only once today (the play we are saving). settings.replace_int('today_plays', 1) if ( last_play_date[0] == yesterday_play_date[0] and @@ -379,7 +380,7 @@ class Base(ABC): # We played yesterday, add one to consecutive days settings.replace_int('consecutive_days', settings.get_int('consecutive_days') + 1) else: - # We haven't played yet today or yesterday, reset consecutive days + # We haven't played yesterday, so we have only one consecutive day. settings.replace_int('consecutive_days', 1) settings.replace_int_array('last_play_date', 3, today_play_date) @@ -413,7 +414,7 @@ class Base(ABC): settings = ValidatedDict() return settings - def get_play_statistics(self, userid: UserID) -> ValidatedDict: + def get_play_statistics(self, userid: UserID) -> PlayStatistics: """ Given a user ID, get the play statistics. @@ -433,8 +434,93 @@ class Base(ABC): consecutive_days - Number of consecutive days played at this time. """ if RemoteUser.is_remote(userid): - return ValidatedDict({}) + return PlayStatistics( + self.game, + 0, + 0, + 0, + 0, + Time.now(), + Time.now(), + ) + + # Grab the last saved settings and today's date. settings = self.data.local.game.get_settings(self.game, userid) + today_play_date = Time.todays_date() + yesterday_play_date = Time.yesterdays_date() if settings is None: - return ValidatedDict({}) - return settings + return PlayStatistics( + self.game, + 1, + 1, + 1, + 1, + Time.now(), + Time.now(), + ) + + # Calculate whether we are on our first play of the day or not. + last_play_date = settings.get_int_array('last_play_date', 3) + if ( + last_play_date[0] == today_play_date[0] and + last_play_date[1] == today_play_date[1] and + last_play_date[2] == today_play_date[2] + ): + # We last played today, so the total days and today plays are accurate + # as stored. + today_count = settings.get_int('today_plays', 0) + total_days = settings.get_int('total_days', 1) + consecutive_days = settings.get_int('consecutive_days', 1) + else: + if ( + last_play_date[0] != 0 and + last_play_date[1] != 0 and + last_play_date[2] != 0 + ): + # We've played before but not today, so the total days is + # the stored count plus today. + total_days = settings.get_int('total_days') + 1 + else: + # We've never played before, so the total days is just 1. + total_days = 1 + + if ( + last_play_date[0] == yesterday_play_date[0] and + last_play_date[1] == yesterday_play_date[1] and + last_play_date[2] == yesterday_play_date[2] + ): + # We've played before, and it was yesterday, so today is the + # next consecutive day. So add the current value and today. + consecutive_days = settings.get_int('consecutive_days') + 1 + else: + # This is the first consecutive day, we've either never played + # or we played a bunch but in the past before yesterday. + consecutive_days = 1 + + # We haven't played yet today. + today_count = 0 + + # Grab any extra settings that a game may have stored here. + extra_settings: Dict[str, Any] = { + key: value for (key, value) in settings.items() + if key not in { + 'total_plays', + 'today_plays', + 'total_days', + 'first_play_timestamp', + 'last_play_timestamp', + 'last_play_date', + 'consecutive_days', + } + } + + return PlayStatistics( + self.game, + settings.get_int('total_plays') + 1, + today_count + 1, + total_days, + consecutive_days, + settings.get_int('first_play_timestamp', Time.now()), + settings.get_int('last_play_timestamp', Time.now()), + extra_settings, + ) diff --git a/bemani/backend/ddr/common.py b/bemani/backend/ddr/common.py index fada83e..db97ff2 100644 --- a/bemani/backend/ddr/common.py +++ b/bemani/backend/ddr/common.py @@ -1,7 +1,7 @@ from typing import Dict, Optional, Tuple from bemani.backend.ddr.base import DDRBase -from bemani.common import Time, Profile, intish +from bemani.common import Profile, intish from bemani.data import Score, UserID from bemani.protocol import Node @@ -272,19 +272,9 @@ class DDRGameLoadDailyHandler(DDRBase): play_stats = self.get_play_statistics(userid) # Day play counts - last_play_date = play_stats.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = play_stats.get_int('today_plays', 0) - else: - today_count = 0 daycount = Node.void('daycount') game.add_child(daycount) - daycount.set_attribute('playcount', str(today_count)) + daycount.set_attribute('playcount', str(play_stats.today_plays)) # Daily combo stuff, unclear how this works dailycombo = Node.void('dailycombo') diff --git a/bemani/backend/ddr/ddr2013.py b/bemani/backend/ddr/ddr2013.py index 188888b..aa3c77c 100644 --- a/bemani/backend/ddr/ddr2013.py +++ b/bemani/backend/ddr/ddr2013.py @@ -333,19 +333,9 @@ class DDR2013( workout.set_attribute('disp', '1') # Daily play counts - last_play_date = play_stats.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = play_stats.get_int('today_plays', 0) - else: - today_count = 0 daycount = Node.void('daycount') root.add_child(daycount) - daycount.set_attribute('playcount', str(today_count)) + daycount.set_attribute('playcount', str(play_stats.today_plays)) # Daily combo stuff, unknown how this works dailycombo = Node.void('dailycombo') diff --git a/bemani/backend/ddr/ddr2014.py b/bemani/backend/ddr/ddr2014.py index b700988..03e5d75 100644 --- a/bemani/backend/ddr/ddr2014.py +++ b/bemani/backend/ddr/ddr2014.py @@ -388,19 +388,9 @@ class DDR2014( totalcalorie.set_attribute('total', str(total)) # Daily play counts - last_play_date = play_stats.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = play_stats.get_int('today_plays', 0) - else: - today_count = 0 daycount = Node.void('daycount') root.add_child(daycount) - daycount.set_attribute('playcount', str(today_count)) + daycount.set_attribute('playcount', str(play_stats.today_plays)) # Daily combo stuff, unknown how this works dailycombo = Node.void('dailycombo') diff --git a/bemani/backend/ddr/ddrx3.py b/bemani/backend/ddr/ddrx3.py index 9475d4a..3180a90 100644 --- a/bemani/backend/ddr/ddrx3.py +++ b/bemani/backend/ddr/ddrx3.py @@ -408,19 +408,9 @@ class DDRX3( workout.set_attribute('disp', '1') # Daily play counts - last_play_date = play_stats.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = play_stats.get_int('today_plays', 0) - else: - today_count = 0 daycount = Node.void('daycount') root.add_child(daycount) - daycount.set_attribute('playcount', str(today_count)) + daycount.set_attribute('playcount', str(play_stats.today_plays)) # Daily combo stuff, unknown how this works dailycombo = Node.void('dailycombo') diff --git a/bemani/backend/museca/museca1.py b/bemani/backend/museca/museca1.py index a536101..891d754 100644 --- a/bemani/backend/museca/museca1.py +++ b/bemani/backend/museca/museca1.py @@ -12,7 +12,7 @@ from bemani.backend.museca.common import ( MusecaGameSaveMusicHandler, MusecaGameShopHandler, ) -from bemani.common import Time, VersionConstants, Profile, ID +from bemani.common import VersionConstants, Profile, ID from bemani.data import UserID from bemani.protocol import Node @@ -215,19 +215,9 @@ class Museca1( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('daily_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('daily_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Last played stuff if 'last' in profile: diff --git a/bemani/backend/museca/museca1plus.py b/bemani/backend/museca/museca1plus.py index 5173f9a..2195c8f 100644 --- a/bemani/backend/museca/museca1plus.py +++ b/bemani/backend/museca/museca1plus.py @@ -13,7 +13,7 @@ from bemani.backend.museca.common import ( MusecaGameShopHandler, ) from bemani.backend.museca.museca1 import Museca1 -from bemani.common import Time, VersionConstants, Profile, ID +from bemani.common import VersionConstants, Profile, ID from bemani.data import UserID from bemani.protocol import Node @@ -354,19 +354,9 @@ class Museca1Plus( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('daily_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('daily_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Last played stuff if 'last' in profile: diff --git a/bemani/backend/popn/eclale.py b/bemani/backend/popn/eclale.py index 5cf0e6c..6f91f2e 100644 --- a/bemani/backend/popn/eclale.py +++ b/bemani/backend/popn/eclale.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.lapistoria import PopnMusicLapistoria -from bemani.common import Time, Profile, VersionConstants +from bemani.common import Profile, VersionConstants from bemani.data import UserID, Link from bemani.protocol import Node @@ -546,20 +546,10 @@ class PopnMusicEclale(PopnMusicBase): # player statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0))) - account.add_child(Node.s16('today_play_cnt', today_count)) - account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) - account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0))) + account.add_child(Node.s16('total_play_cnt', statistics.total_plays)) + account.add_child(Node.s16('today_play_cnt', statistics.today_plays)) + account.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) + account.add_child(Node.s16('total_days', statistics.total_days)) account.add_child(Node.s16('interval_day', 0)) # Set up info node diff --git a/bemani/backend/popn/fantasia.py b/bemani/backend/popn/fantasia.py index 14ba57f..c9a593d 100644 --- a/bemani/backend/popn/fantasia.py +++ b/bemani/backend/popn/fantasia.py @@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.tunestreet import PopnMusicTuneStreet from bemani.backend.base import Status -from bemani.common import Profile, VersionConstants, Time, ID +from bemani.common import Profile, VersionConstants, ID from bemani.data import Score, Link, UserID from bemani.protocol import Node @@ -120,19 +120,9 @@ class PopnMusicFantasia(PopnMusicBase): # Statistics section and scores section statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0))) - base.add_child(Node.s16('today_play_cnt', today_count)) - base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) + base.add_child(Node.s32('total_play_cnt', statistics.total_plays)) + base.add_child(Node.s16('today_play_cnt', statistics.today_plays)) + base.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) # Number of rivals that are active for this version. links = self.data.local.user.get_links(self.game, self.version, userid) diff --git a/bemani/backend/popn/lapistoria.py b/bemani/backend/popn/lapistoria.py index 9d0389d..eb94f45 100644 --- a/bemani/backend/popn/lapistoria.py +++ b/bemani/backend/popn/lapistoria.py @@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.sunnypark import PopnMusicSunnyPark from bemani.backend.base import Status -from bemani.common import Profile, VersionConstants, Time, ID +from bemani.common import Profile, VersionConstants, ID from bemani.data import UserID, Link from bemani.protocol import Node @@ -340,20 +340,10 @@ class PopnMusicLapistoria(PopnMusicBase): # Statistics section and scores section statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0))) - account.add_child(Node.s16('today_play_cnt', today_count)) - account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) - account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0))) + account.add_child(Node.s16('total_play_cnt', statistics.total_plays)) + account.add_child(Node.s16('today_play_cnt', statistics.today_plays)) + account.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) + account.add_child(Node.s16('total_days', statistics.total_days)) account.add_child(Node.s16('interval_day', 0)) # Number of rivals that are active for this version. diff --git a/bemani/backend/popn/sunnypark.py b/bemani/backend/popn/sunnypark.py index 6701799..7396cee 100644 --- a/bemani/backend/popn/sunnypark.py +++ b/bemani/backend/popn/sunnypark.py @@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.fantasia import PopnMusicFantasia from bemani.backend.base import Status -from bemani.common import Profile, VersionConstants, Time, ID +from bemani.common import Profile, VersionConstants, ID from bemani.data import UserID, Link from bemani.protocol import Node @@ -81,19 +81,9 @@ class PopnMusicSunnyPark(PopnMusicBase): # Statistics section and scores section statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0))) - base.add_child(Node.s16('today_play_cnt', today_count)) - base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) + base.add_child(Node.s32('total_play_cnt', statistics.total_plays)) + base.add_child(Node.s16('today_play_cnt', statistics.today_plays)) + base.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) # Number of rivals that are active for this version. links = self.data.local.user.get_links(self.game, self.version, userid) diff --git a/bemani/backend/popn/usaneko.py b/bemani/backend/popn/usaneko.py index 7f2495d..61acd76 100644 --- a/bemani/backend/popn/usaneko.py +++ b/bemani/backend/popn/usaneko.py @@ -796,7 +796,6 @@ class PopnMusicUsaNeko(PopnMusicBase): root.add_child(account) account.add_child(Node.string('g_pm_id', self.format_extid(profile.extid))) account.add_child(Node.string('name', profile.get_str('name', 'なし'))) - account.add_child(Node.s16('tutorial', profile.get_int('tutorial'))) account.add_child(Node.s16('area_id', profile.get_int('area_id'))) account.add_child(Node.s16('use_navi', profile.get_int('use_navi'))) account.add_child(Node.s16('read_news', profile.get_int('read_news'))) @@ -817,6 +816,53 @@ class PopnMusicUsaNeko(PopnMusicBase): account.add_child(Node.s32('player_point', profile.get_int('player_point', 300))) account.add_child(Node.s32_array('power_point_list', profile.get_int_array('power_point_list', 20, [-1] * 20))) + # Tutorial handling is all sorts of crazy in UsaNeko. the tutorial flag + # is split into two values. The game uses the flag modulo 100 for standard + # tutorial progress, and the flag divided by 100 for the hold note tutorial. + # The hold note tutorial will activate the first time you choose a song with + # hold notes in it, regardless of whether you say yes/no. The total times you + # have ever played Pop'n Music also factors in for some screens. The enumerated + # values are as follows: + # + # Lower values: + # 0 - Should not be used, presenting this to the game causes buggy behavior. + # 1 - User has not been prompted to choose any tutorials. Prompts the user for the + # menu tutorial. If the user selects "no" then moves the tutorial state to + # "2" at the end of the round. If the user selects "yes" then moves the + # tutorial state to "3" immediately and starts the menu tutorial. If the total + # play count for this user is "1" when this value is hit, the game will bug + # out and play the hold note tutorial and then crash. + # 2 - Prompt the user on the mode select screen asking them if they want to see + # the menu tutorial. If the user selects "no" then moves the tutorial state + # to "8" immediately. If the user selects "yes" then moves the tutorial state + # to "3" immediately. If the total play count for this user is "1" when this value + # is hit, then the game will bug out and play the hold note tutorial and then crash. + # 3 - Display some tutorial elements on most screens, and then advance the tutorial + # state to "4" on profile save. + # 4 - Display some tutorial elements on most screens, and then advance the tutorial + # state to "5" on profile save. + # 5 - Display some tutorial elements on most screens, and then prompt user with a + # repeat tutorial question. If the user selects "no" then moves the tutorial + # state to "8". If the user selects "yes" then moves the tutorial state to "3". + # 6 - Do nothing, display nothing, but advance the tutorial state to "7" at the + # end of the game. It seems that nothing requests this state. + # 7 - Display guide information prompt on the option select screen. Game moves + # this to "8" after this tutorial has been displayed. + # 8 - Do not display any more tutorial stuff, this is a terminal state. + # + # Upper values: + # 0 - Should not be used, presenting this to the game causes buggy behavior. + # 1 - Hold note tutorial has not been activated yet and will be displayed when + # the player chooses a song with hold notes. Game moves this to "2" after this + # tutorial has been activated. + # 2 - Hold note tutorial was displayed to the user, but the mini-tutorial showing + # the hold note indicator that pops up after the hold note tutorial has not + # been displayed yet. Presumably this is just in case you play a hold note + # song on your last stage. Game moves this to "3" after this tutorial has been + # displayed. + # 3 - All hold note tutorials are finished, this is a terminal state. + account.add_child(Node.s16('tutorial', profile.get_int('tutorial'))) + # Stuff we never change account.add_child(Node.s8('staff', 0)) account.add_child(Node.s16('item_type', 0)) @@ -837,20 +883,10 @@ class PopnMusicUsaNeko(PopnMusicBase): # Player statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0))) - account.add_child(Node.s16('today_play_cnt', today_count)) - account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) - account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0))) + account.add_child(Node.s16('total_play_cnt', statistics.total_plays)) + account.add_child(Node.s16('today_play_cnt', statistics.today_plays)) + account.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) + account.add_child(Node.s16('total_days', statistics.total_days)) account.add_child(Node.s16('interval_day', 0)) # Number of rivals that are active for this version. diff --git a/bemani/backend/reflec/colette.py b/bemani/backend/reflec/colette.py index df065e1..db86b08 100644 --- a/bemani/backend/reflec/colette.py +++ b/bemani/backend/reflec/colette.py @@ -705,26 +705,14 @@ class ReflecBeatColette(ReflecBeatBase): pdata = Node.void('pdata') root.add_child(pdata) - # Account time info - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - account = Node.void('account') pdata.add_child(account) account.add_child(Node.s32('usrid', profile.extid)) - account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0))) - account.add_child(Node.s32('dpc', today_count)) + account.add_child(Node.s32('tpc', statistics.total_plays)) + account.add_child(Node.s32('dpc', statistics.today_plays)) account.add_child(Node.s32('crd', 1)) account.add_child(Node.s32('brd', 1)) - account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0))) + account.add_child(Node.s32('tdc', statistics.total_days)) account.add_child(Node.s32('intrvld', 0)) account.add_child(Node.s16('ver', 5)) account.add_child(Node.u64('pst', 0)) diff --git a/bemani/backend/reflec/groovin.py b/bemani/backend/reflec/groovin.py index 275ac60..df3e68d 100644 --- a/bemani/backend/reflec/groovin.py +++ b/bemani/backend/reflec/groovin.py @@ -927,27 +927,15 @@ class ReflecBeatGroovin(ReflecBeatBase): pdata = Node.void('pdata') root.add_child(pdata) - # Account time info - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - # Account info account = Node.void('account') pdata.add_child(account) account.add_child(Node.s32('usrid', profile.extid)) - account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0))) - account.add_child(Node.s32('dpc', today_count)) + account.add_child(Node.s32('tpc', statistics.total_plays)) + account.add_child(Node.s32('dpc', statistics.today_plays)) account.add_child(Node.s32('crd', 1)) account.add_child(Node.s32('brd', 1)) - account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0))) + account.add_child(Node.s32('tdc', statistics.total_days)) account.add_child(Node.s32('intrvld', 0)) account.add_child(Node.s16('ver', 1)) account.add_child(Node.u64('pst', 0)) diff --git a/bemani/backend/reflec/limelight.py b/bemani/backend/reflec/limelight.py index 26ba638..50bfd97 100644 --- a/bemani/backend/reflec/limelight.py +++ b/bemani/backend/reflec/limelight.py @@ -563,23 +563,12 @@ class ReflecBeatLimelight(ReflecBeatBase): base.add_child(Node.s32('pc', profile.get_int('pc'))) base.add_child(Node.s32('uattr', profile.get_int('uattr'))) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - con = Node.void('con') pdata.add_child(con) - con.add_child(Node.s32('day', today_count)) - con.add_child(Node.s32('cnt', statistics.get_int('total_plays'))) - con.add_child(Node.s32('total_cnt', statistics.get_int('total_plays'))) - con.add_child(Node.s32('last', statistics.get_int('last_play_timestamp'))) + con.add_child(Node.s32('day', statistics.today_plays)) + con.add_child(Node.s32('cnt', statistics.total_plays)) + con.add_child(Node.s32('total_cnt', statistics.total_plays)) + con.add_child(Node.s32('last', statistics.last_play_timestamp)) con.add_child(Node.s32('now', Time.now())) team = Node.void('team') diff --git a/bemani/backend/reflec/reflecbeat.py b/bemani/backend/reflec/reflecbeat.py index 4890ff3..026eca5 100644 --- a/bemani/backend/reflec/reflecbeat.py +++ b/bemani/backend/reflec/reflecbeat.py @@ -321,22 +321,11 @@ class ReflecBeat(ReflecBeatBase): base.add_child(Node.s16('ap', profile.get_int('ap'))) base.add_child(Node.s32('flag', profile.get_int('flag'))) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - con = Node.void('con') pdata.add_child(con) - con.add_child(Node.s32('day', today_count)) - con.add_child(Node.s32('cnt', statistics.get_int('total_plays'))) - con.add_child(Node.s32('last', statistics.get_int('last_play_timestamp'))) + con.add_child(Node.s32('day', statistics.today_plays)) + con.add_child(Node.s32('cnt', statistics.total_plays)) + con.add_child(Node.s32('last', statistics.last_play_timestamp)) con.add_child(Node.s32('now', Time.now())) team = Node.void('team') diff --git a/bemani/backend/reflec/volzza.py b/bemani/backend/reflec/volzza.py index c32bccb..a577f62 100644 --- a/bemani/backend/reflec/volzza.py +++ b/bemani/backend/reflec/volzza.py @@ -310,18 +310,6 @@ class ReflecBeatVolzza(ReflecBeatVolzzaBase): pdata = Node.void('pdata') root.add_child(pdata) - # Account time info - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - # Previous account info previous_version = self.previous_version() if previous_version: @@ -333,11 +321,11 @@ class ReflecBeatVolzza(ReflecBeatVolzzaBase): account = Node.void('account') pdata.add_child(account) account.add_child(Node.s32('usrid', profile.extid)) - account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0))) - account.add_child(Node.s32('dpc', today_count)) + account.add_child(Node.s32('tpc', statistics.total_plays)) + account.add_child(Node.s32('dpc', statistics.today_plays)) account.add_child(Node.s32('crd', 1)) account.add_child(Node.s32('brd', 1)) - account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0))) + account.add_child(Node.s32('tdc', statistics.total_days)) account.add_child(Node.s32('intrvld', 0)) account.add_child(Node.s16('ver', 0)) account.add_child(Node.u64('pst', 0)) diff --git a/bemani/backend/reflec/volzza2.py b/bemani/backend/reflec/volzza2.py index 633fc35..9189716 100644 --- a/bemani/backend/reflec/volzza2.py +++ b/bemani/backend/reflec/volzza2.py @@ -330,18 +330,6 @@ class ReflecBeatVolzza2(ReflecBeatVolzzaBase): pdata = Node.void('pdata') root.add_child(pdata) - # Account time info - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - # Previous account info previous_version = self.previous_version() if previous_version: @@ -353,11 +341,11 @@ class ReflecBeatVolzza2(ReflecBeatVolzzaBase): account = Node.void('account') pdata.add_child(account) account.add_child(Node.s32('usrid', profile.extid)) - account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0))) - account.add_child(Node.s32('dpc', today_count)) + account.add_child(Node.s32('tpc', statistics.total_plays)) + account.add_child(Node.s32('dpc', statistics.today_plays)) account.add_child(Node.s32('crd', 1)) account.add_child(Node.s32('brd', 1)) - account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0))) + account.add_child(Node.s32('tdc', statistics.total_days)) account.add_child(Node.s32('intrvld', 0)) account.add_child(Node.s16('ver', 0)) account.add_child(Node.bool('succeed', succeeded)) diff --git a/bemani/backend/sdvx/gravitywars_s1.py b/bemani/backend/sdvx/gravitywars_s1.py index 93f3491..edee765 100644 --- a/bemani/backend/sdvx/gravitywars_s1.py +++ b/bemani/backend/sdvx/gravitywars_s1.py @@ -3,7 +3,7 @@ import copy from typing import Any, Dict, List from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars -from bemani.common import ID, Time, Profile +from bemani.common import ID, Profile from bemani.data import UserID from bemani.protocol import Node @@ -3149,19 +3149,9 @@ class SoundVoltexGravityWarsSeason1( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('daily_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('daily_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Last played stuff if 'last' in profile: diff --git a/bemani/backend/sdvx/gravitywars_s2.py b/bemani/backend/sdvx/gravitywars_s2.py index 1432982..ffc27d3 100644 --- a/bemani/backend/sdvx/gravitywars_s2.py +++ b/bemani/backend/sdvx/gravitywars_s2.py @@ -3,7 +3,7 @@ import copy from typing import Any, Dict, List, Tuple from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars -from bemani.common import ID, Time, Profile +from bemani.common import ID, Profile from bemani.data import Score, UserID from bemani.protocol import Node @@ -3970,19 +3970,9 @@ class SoundVoltexGravityWarsSeason2( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('daily_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('daily_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Last played stuff if 'last' in profile: diff --git a/bemani/backend/sdvx/heavenlyhaven.py b/bemani/backend/sdvx/heavenlyhaven.py index 3e2fbae..66c8078 100644 --- a/bemani/backend/sdvx/heavenlyhaven.py +++ b/bemani/backend/sdvx/heavenlyhaven.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple from bemani.backend.ess import EventLogHandler from bemani.backend.sdvx.base import SoundVoltexBase from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars -from bemani.common import ID, Time, Profile, VersionConstants +from bemani.common import ID, Profile, VersionConstants from bemani.data import Score, UserID from bemani.protocol import Node @@ -3858,19 +3858,9 @@ class SoundVoltexHeavenlyHaven( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('today_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('today_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Also exists but we don't support: # - day_count: Number of days where this user had at least one play. diff --git a/bemani/backend/sdvx/infiniteinfection.py b/bemani/backend/sdvx/infiniteinfection.py index 913557b..bee7083 100644 --- a/bemani/backend/sdvx/infiniteinfection.py +++ b/bemani/backend/sdvx/infiniteinfection.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional from bemani.backend.ess import EventLogHandler from bemani.backend.sdvx.base import SoundVoltexBase from bemani.backend.sdvx.booth import SoundVoltexBooth -from bemani.common import Time, Profile, VersionConstants, ID +from bemani.common import Profile, VersionConstants, ID from bemani.data import UserID from bemani.protocol import Node @@ -2304,19 +2304,9 @@ class SoundVoltexInfiniteInfection( # Play statistics statistics = self.get_play_statistics(userid) - last_play_date = statistics.get_int_array('last_play_date', 3) - today_play_date = Time.todays_date() - if ( - last_play_date[0] == today_play_date[0] and - last_play_date[1] == today_play_date[1] and - last_play_date[2] == today_play_date[2] - ): - today_count = statistics.get_int('today_plays', 0) - else: - today_count = 0 - game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0))) - game.add_child(Node.u32('daily_count', today_count)) - game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0))) + game.add_child(Node.u32('play_count', statistics.total_plays)) + game.add_child(Node.u32('daily_count', statistics.today_plays)) + game.add_child(Node.u32('play_chain', statistics.consecutive_days)) # Last played stuff if 'last' in profile: diff --git a/bemani/common/__init__.py b/bemani/common/__init__.py index 9a44aef..0ba9eb6 100644 --- a/bemani/common/__init__.py +++ b/bemani/common/__init__.py @@ -1,5 +1,5 @@ from bemani.common.model import Model -from bemani.common.validateddict import ValidatedDict, Profile, intish +from bemani.common.validateddict import ValidatedDict, Profile, PlayStatistics, intish from bemani.common.http import HTTP from bemani.common.constants import APIConstants, GameConstants, VersionConstants, DBConstants, BroadcastConstants from bemani.common.card import CardCipher, CardCipherException @@ -14,6 +14,7 @@ __all__ = [ "Model", "ValidatedDict", "Profile", + "PlayStatistics", "HTTP", "APIConstants", "GameConstants", diff --git a/bemani/common/validateddict.py b/bemani/common/validateddict.py index 272bba7..3a0f131 100644 --- a/bemani/common/validateddict.py +++ b/bemani/common/validateddict.py @@ -454,3 +454,37 @@ class Profile(ValidatedDict): self.version = version self.refid = refid self.extid = extid + + +class PlayStatistics(ValidatedDict): + """ + A special case of a ValidatedDict, a play statistics object is guaranteed + to also contain several values representing last play times, total play times, + and the like. + """ + + def __init__( + self, + game: GameConstants, + total_plays: int, + today_plays: int, + total_days: int, + consecutive_days: int, + first_play_timestamp: int, + last_play_timestamp: int, + extra_values: Dict[str, Any] = {}, + ) -> None: + super().__init__(extra_values or {}) + self.game = game + # How many actual profiles saves have we registered across all games in this series. + self.total_plays = total_plays + # How many actual profile saves have we registered today, so far. + self.today_plays = today_plays + # How many total days that we have registered at least one profile save. + self.total_days = total_days + # How many consecutive days in a row we registered at least one profile save. + self.consecutive_days = consecutive_days + # The timestamp of the very first play session, in seconds. + self.first_play_timestamp = first_play_timestamp + # The timestamp of the very last play session, in seconds. + self.last_play_timestamp = last_play_timestamp diff --git a/bemani/data/mysql/api.py b/bemani/data/mysql/api.py index 2ae24da..6ff21a5 100644 --- a/bemani/data/mysql/api.py +++ b/bemani/data/mysql/api.py @@ -86,7 +86,7 @@ class APIData(APIProviderInterface, BaseData): cursor = self.execute( sql, { - 'timestamp': int(Time.now()), + 'timestamp': Time.now(), 'name': name, 'token': str(uuid.uuid4()), }, @@ -175,7 +175,7 @@ class APIData(APIProviderInterface, BaseData): cursor = self.execute( sql, { - 'timestamp': int(Time.now()), + 'timestamp': Time.now(), 'uri': uri, 'token': token, }, diff --git a/bemani/data/mysql/network.py b/bemani/data/mysql/network.py index b2109a7..9f5c167 100644 --- a/bemani/data/mysql/network.py +++ b/bemani/data/mysql/network.py @@ -88,7 +88,7 @@ class NetworkData(BaseData): The ID of the newly created entry. """ sql = "INSERT INTO news (timestamp, title, body) VALUES (:timestamp, :title, :body)" - cursor = self.execute(sql, {'timestamp': int(Time.now()), 'title': title, 'body': body}) + cursor = self.execute(sql, {'timestamp': Time.now(), 'title': title, 'body': body}) return cursor.lastrowid def get_news(self, newsid: int) -> Optional[News]: diff --git a/bemani/tests/test_PlayStats.py b/bemani/tests/test_PlayStats.py new file mode 100644 index 0000000..36060c0 --- /dev/null +++ b/bemani/tests/test_PlayStats.py @@ -0,0 +1,260 @@ +# vim: set fileencoding=utf-8 +import unittest +from freezegun import freeze_time +from typing import Dict, Any +from unittest.mock import Mock + +from bemani.backend.base import Base +from bemani.common import GameConstants, Time, ValidatedDict +from bemani.data import UserID + + +# Make the normally-abstract base instantiable so we can test it. +class InstantiableBase(Base): + game = GameConstants.BISHI_BASHI + version = -1 + name = "Test Class" + + +# Make an easier mock implementation of load/save stats. +def mock_stats(existing_value: Dict[str, Any]) -> Mock: + data = Mock() + data.local = Mock() + data.local.game = Mock() + data.local.game.get_settings = Mock(return_value=ValidatedDict(existing_value) if existing_value else None) + data.local.game.put_settings = Mock() + return data + + +def saved_stats(mock: Mock) -> ValidatedDict: + return ValidatedDict(mock.local.game.put_settings.call_args.args[2]) + + +class TestPlayStats(unittest.TestCase): + + def test_get_brand_new_profile(self) -> None: + with freeze_time('2021-08-24'): + stats = None + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + + self.assertEqual(settings.game, GameConstants.BISHI_BASHI) + self.assertEqual(settings.total_plays, 1) + self.assertEqual(settings.today_plays, 1) + self.assertEqual(settings.total_days, 1) + self.assertEqual(settings.consecutive_days, 1) + self.assertEqual(settings.first_play_timestamp, Time.now()) + self.assertEqual(settings.last_play_timestamp, Time.now()) + + def test_put_brand_new_profile(self) -> None: + with freeze_time('2021-08-24'): + stats = None + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + base.update_play_statistics(UserID(1337), settings) + new_settings = saved_stats(data) + + self.assertEqual(new_settings.get_int('total_plays'), 1) + self.assertEqual(new_settings.get_int('today_plays'), 1) + self.assertEqual(new_settings.get_int('total_days'), 1) + self.assertEqual(new_settings.get_int('consecutive_days'), 1) + self.assertEqual(new_settings.get_int('first_play_timestamp'), Time.now()) + self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now()) + self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date()) + + def test_get_played_today(self) -> None: + with freeze_time('2021-08-24'): + play_date = Time.todays_date() + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 9876543210, + 'consecutive_days': 69, + 'last_play_date': [play_date[0], play_date[1], play_date[2]], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + + self.assertEqual(settings.game, GameConstants.BISHI_BASHI) + self.assertEqual(settings.total_plays, 1235) + self.assertEqual(settings.today_plays, 421) + self.assertEqual(settings.total_days, 10) + self.assertEqual(settings.consecutive_days, 69) + self.assertEqual(settings.first_play_timestamp, 1234567890) + self.assertEqual(settings.last_play_timestamp, 9876543210) + + def test_put_played_today(self) -> None: + with freeze_time('2021-08-24'): + play_date = Time.todays_date() + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 1234567890, + 'consecutive_days': 69, + 'last_play_date': [play_date[0], play_date[1], play_date[2]], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + base.update_play_statistics(UserID(1337), settings) + new_settings = saved_stats(data) + + self.assertEqual(new_settings.get_int('total_plays'), 1235) + self.assertEqual(new_settings.get_int('today_plays'), 421) + self.assertEqual(new_settings.get_int('total_days'), 10) + self.assertEqual(new_settings.get_int('consecutive_days'), 69) + self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890) + self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now()) + self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date()) + + def test_get_played_yesterday(self) -> None: + with freeze_time('2021-08-24'): + play_date = Time.yesterdays_date() + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 9876543210, + 'consecutive_days': 69, + 'last_play_date': [play_date[0], play_date[1], play_date[2]], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + + self.assertEqual(settings.game, GameConstants.BISHI_BASHI) + self.assertEqual(settings.total_plays, 1235) + self.assertEqual(settings.today_plays, 1) + self.assertEqual(settings.total_days, 11) + self.assertEqual(settings.consecutive_days, 70) + self.assertEqual(settings.first_play_timestamp, 1234567890) + self.assertEqual(settings.last_play_timestamp, 9876543210) + + def test_put_played_yesterday(self) -> None: + with freeze_time('2021-08-24'): + play_date = Time.yesterdays_date() + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 1234567890, + 'consecutive_days': 69, + 'last_play_date': [play_date[0], play_date[1], play_date[2]], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + base.update_play_statistics(UserID(1337), settings) + new_settings = saved_stats(data) + + self.assertEqual(new_settings.get_int('total_plays'), 1235) + self.assertEqual(new_settings.get_int('today_plays'), 1) + self.assertEqual(new_settings.get_int('total_days'), 11) + self.assertEqual(new_settings.get_int('consecutive_days'), 70) + self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890) + self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now()) + self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date()) + + def test_get_played_awhile_ago(self) -> None: + with freeze_time('2021-08-24'): + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 9876543210, + 'consecutive_days': 69, + 'last_play_date': [2010, 4, 20], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + + self.assertEqual(settings.game, GameConstants.BISHI_BASHI) + self.assertEqual(settings.total_plays, 1235) + self.assertEqual(settings.today_plays, 1) + self.assertEqual(settings.total_days, 11) + self.assertEqual(settings.consecutive_days, 1) + self.assertEqual(settings.first_play_timestamp, 1234567890) + self.assertEqual(settings.last_play_timestamp, 9876543210) + + def test_put_played_awhile_ago(self) -> None: + with freeze_time('2021-08-24'): + stats = { + 'total_plays': 1234, + 'today_plays': 420, + 'total_days': 10, + 'first_play_timestamp': 1234567890, + 'last_play_timestamp': 1234567890, + 'consecutive_days': 69, + 'last_play_date': [2010, 4, 20], + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + base.update_play_statistics(UserID(1337), settings) + new_settings = saved_stats(data) + + self.assertEqual(new_settings.get_int('total_plays'), 1235) + self.assertEqual(new_settings.get_int('today_plays'), 1) + self.assertEqual(new_settings.get_int('total_days'), 11) + self.assertEqual(new_settings.get_int('consecutive_days'), 1) + self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890) + self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now()) + self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date()) + + def test_get_extra_settings(self) -> None: + with freeze_time('2021-08-24'): + stats = { + 'total_plays': 1234, + 'key': 'value', + 'int': 1337, + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + + self.assertEqual(settings.get_int('int'), 1337) + self.assertEqual(settings.get_str('key'), 'value') + self.assertEqual(settings.get_int('total_plays'), 0) + + def test_put_extra_settings(self) -> None: + with freeze_time('2021-08-24'): + stats = { + 'total_plays': 1234, + 'key': 'value', + 'int': 1337, + } + data = mock_stats(stats) + base = InstantiableBase(data, Mock(), Mock()) + + settings = base.get_play_statistics(UserID(1337)) + settings.replace_int('int', 420) + settings.replace_int('int2', 69) + settings.replace_int('total_plays', 37) + base.update_play_statistics(UserID(1337), settings) + + new_settings = saved_stats(data) + + self.assertEqual(new_settings.get_int('int'), 420) + self.assertEqual(new_settings.get_str('key'), 'value') + self.assertEqual(new_settings.get_int('int2'), 69) + self.assertEqual(new_settings.get_int('total_plays'), 1235)