Add accuracy curve (accgraph) strategy for BeatLeader that filters by a star range

This commit is contained in:
Brian Lee 2025-01-26 08:28:45 -08:00
parent a099898e4e
commit ff6e2e4968
2 changed files with 185 additions and 7 deletions

View File

@ -21,10 +21,25 @@ from helpers.SimpleBeatLeaderAPI import SimpleBeatLeaderAPI
from helpers.SimpleBeatSaverAPI import SimpleBeatSaverAPI
from saberlist.utils import reset_history
from saberlist.playlist_strategies.oldscores import playlist_strategy_beatleader_oldscores, playlist_strategy_scoresaber_oldscores
from saberlist.playlist_strategies.accuracy import playlist_strategy_beatleader_lowest_acc, playlist_strategy_beatleader_accuracy_gaps, playlist_strategy_scoresaber_accuracy_gaps
from saberlist.playlist_strategies.performance import playlist_strategy_beatleader_lowest_pp, playlist_strategy_scoresaber_lowest_pp
from saberlist.playlist_strategies.beatsaver import playlist_strategy_beatsaver_acc, playlist_strategy_beatsaver_curated, playlist_strategy_beatsaver_mappers
from saberlist.playlist_strategies.oldscores import (
playlist_strategy_beatleader_oldscores,
playlist_strategy_scoresaber_oldscores,
)
from saberlist.playlist_strategies.accuracy import (
playlist_strategy_beatleader_lowest_acc,
playlist_strategy_beatleader_accuracy_gaps,
playlist_strategy_scoresaber_accuracy_gaps,
playlist_strategy_beatleader_accuracy_gaps_star_range,
)
from saberlist.playlist_strategies.performance import (
playlist_strategy_beatleader_lowest_pp,
playlist_strategy_scoresaber_lowest_pp,
)
from saberlist.playlist_strategies.beatsaver import (
playlist_strategy_beatsaver_acc,
playlist_strategy_beatsaver_curated,
playlist_strategy_beatsaver_mappers,
)
def saberlist() -> None:
"""
@ -44,7 +59,7 @@ def saberlist() -> None:
playlist_builder = PlaylistBuilder(covers_dir='./covers/beatsavers')
elif strategy == 'beatleader_lowest_pp':
playlist_data, playlist_title = playlist_strategy_beatleader_lowest_pp(BeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
playlist_builder = PlaylistBuilder(covers_dir='./covers/beatleader')
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
elif strategy == 'scoresaber_lowest_pp':
playlist_data, playlist_title = playlist_strategy_scoresaber_lowest_pp(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
playlist_builder = PlaylistBuilder(covers_dir='./covers/scoresaber')
@ -54,6 +69,10 @@ def saberlist() -> None:
elif strategy == 'beatleader_accuracy_gaps':
playlist_data, playlist_title = playlist_strategy_beatleader_accuracy_gaps(SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
elif strategy == 'beatleader_accuracy_gaps_star_range':
input_star_level = input("Enter star level (Default: 6)") or 6
playlist_data, playlist_title = playlist_strategy_beatleader_accuracy_gaps_star_range(SimpleBeatLeaderAPI(cache_expiry_days=CACHE_EXPIRY_DAYS), star_level=float(input_star_level))
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
elif strategy == 'scoresaber_accuracy_gaps':
playlist_data, playlist_title = playlist_strategy_scoresaber_accuracy_gaps(ScoreSaberAPI(cache_expiry_days=CACHE_EXPIRY_DAYS))
playlist_builder = PlaylistBuilder(covers_dir='./covers/scoresaber')
@ -63,11 +82,14 @@ def saberlist() -> None:
elif strategy == 'beatsaver_mappers':
playlist_data, playlist_title = playlist_strategy_beatsaver_mappers(SimpleBeatSaverAPI())
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
elif strategy == 'blank_playlist':
playlist_data, playlist_title = [], input("Enter playlist title: ")
playlist_builder = PlaylistBuilder(covers_dir='./covers/pajamas')
else:
logging.error(f"Unknown strategy '{strategy}'")
return
if not playlist_data:
if not playlist_data and strategy != 'blank_playlist':
logging.info("No new scores found to add to the playlist.")
return
@ -88,9 +110,11 @@ def get_strategy():
# "scoresaber_lowest_pp",
# "beatleader_lowest_acc",
"beatleader_accuracy_gaps",
"beatleader_accuracy_gaps_star_range",
"scoresaber_accuracy_gaps",
"beatsaver_curated",
"beatsaver_mappers"
"beatsaver_mappers",
"blank_playlist"
],
help="Specify the playlist generation strategy")
parser.add_argument("-r", "--reset",

View File

@ -391,3 +391,157 @@ def playlist_strategy_beatleader_lowest_acc(
save_history(history)
return playlist_data, f"beatleader_lowest_acc-{new_count:02d}"
def playlist_strategy_beatleader_accuracy_gaps_star_range(
api: SimpleBeatLeaderAPI,
star_level: float,
song_count: int = 40,
bin_size: float = 0.25,
bin_sort: bool = False
) -> List[Dict[str, Any]]:
"""
Build a playlist of songs where the player's accuracy is furthest below the median accuracy
for a specified star rating range. Songs are grouped into bins by star rating within the
specified range to ensure fair comparison.
Like beatleader_accuracy_gaps, but only for a specific star rating range.
:param api: SimpleBeatLeaderAPI instance for making API calls
:param star_level: Specific star level to filter songs (e.g., 6.0 for songs ranging from 6.00 to 6.99)
:param song_count: Number of songs to include in the playlist
:param bin_size: Size of star rating bins for grouping similar difficulty songs
:param bin_sort: Whether to sort the bins by star rating
:return: A tuple containing (list of song dictionaries, playlist title string)
"""
player_id = prompt_for_player_id()
history = load_history()
history.setdefault('beatleader_accuracy_gaps_star_range', {})
history.setdefault('playlist_counts', {})
# Validate star_level
if not isinstance(star_level, (int, float)) or star_level < 0:
logging.error("Invalid star_level provided. It must be a non-negative number.")
return [], ""
# Define star range based on the specified star_level
star_min = star_level
star_max = star_level + 0.99 # Inclusive of the specified star_level up to the next whole number minus one cent
# Get the current count and increment it
count_key = f'beatleader_accuracy_gaps_star_{star_level}'
current_count = history['playlist_counts'].get(count_key, 0)
new_count = current_count + 1
history['playlist_counts'][count_key] = new_count
# Fetch accuracy graph data
all_scores = api.get_player_accgraph(player_id)
if not all_scores:
logging.warning(f"No accgraph data found for player ID {player_id} on BeatLeader.")
return [], ""
logging.debug(f"Found {len(all_scores)} accgraph entries for player ID {player_id} on BeatLeader.")
# Filter scores within the specified star range
filtered_scores = [
entry for entry in all_scores
if entry.get('stars') is not None and star_min <= entry['stars'] <= star_max
]
if not filtered_scores:
logging.warning(f"No accgraph entries found within star range {star_min} to {star_max}.")
return [], ""
logging.debug(f"Found {len(filtered_scores)} accgraph entries within star range {star_min} to {star_max}.")
# Collect all star ratings within the specified range
star_ratings = [entry['stars'] for entry in filtered_scores if entry.get('stars') is not None]
min_stars = min(star_ratings)
max_stars = max(star_ratings)
star_range = max_stars - min_stars
# Determine number of bins
num_bins = math.ceil(star_range / bin_size)
logging.info(f"Using bin size: {bin_size}, resulting in {num_bins} bins within star range {star_min} to {star_max}.")
# Group accuracies by bins
bin_to_accuracies = defaultdict(list)
for entry in filtered_scores:
stars = entry.get('stars')
acc = entry.get('acc')
if stars is None or acc is None:
continue
bin_index = int((stars - min_stars) / bin_size)
bin_to_accuracies[bin_index].append(acc)
# Calculate median accuracy for each bin
bin_to_median = {}
for bin_index, accs in bin_to_accuracies.items():
bin_to_median[bin_index] = median(accs)
bin_start = min_stars + bin_index * bin_size
bin_end = bin_start + bin_size
logging.debug(f"Median accuracy for bin {bin_index} (stars {bin_start:.2f} to {bin_end:.2f}): {bin_to_median[bin_index]:.4f}")
# Compute difference from median for each score
for entry in filtered_scores:
stars = entry.get('stars')
acc = entry.get('acc')
if stars is not None and acc is not None:
bin_index = int((stars - min_stars) / bin_size)
median_acc = bin_to_median.get(bin_index)
if median_acc is not None:
entry['diff_from_median'] = acc - median_acc
else:
entry['diff_from_median'] = float('inf') # Place entries with missing data at the end
else:
entry['diff_from_median'] = float('inf') # Place entries with missing data at the end
# Sort scores by difference from median (ascending: most below median first)
filtered_scores.sort(key=lambda x: x.get('diff_from_median', float('inf')))
playlist_data = []
for score_entry in filtered_scores:
if len(playlist_data) >= song_count:
break
acc = score_entry.get('acc', 0)
stars = score_entry.get('stars')
song_hash = score_entry.get('hash')
if not song_hash or stars is None:
logging.debug(f"Skipping entry due to missing hash or stars: {score_entry}")
continue
difficulty = score_entry.get('diff', '')
difficulty_characteristic = score_entry.get('mode', 'Standard')
if song_hash in history['beatleader_accuracy_gaps_star_range'] and difficulty in history['beatleader_accuracy_gaps_star_range'][song_hash]:
logging.debug(f"Skipping song {song_hash} with difficulty {difficulty} as it's in history.")
continue
song_dict = {
'hash': song_hash,
'difficulties': [
{
'name': difficulty,
'characteristic': difficulty_characteristic
}
]
}
playlist_data.append(song_dict)
logging.debug(f"Selected song for playlist: Hash={song_hash}, Difficulty={difficulty}, "
f"Accuracy={acc*100:.2f}%, Diff from Median={score_entry['diff_from_median']*100:.2f}%")
# Update history
history['beatleader_accuracy_gaps_star_range'].setdefault(song_hash, []).append(difficulty)
if not playlist_data:
logging.info(f"No new songs found to add to the playlist based on history for BeatLeader accuracy gaps within star range {star_min} to {star_max}.")
else:
for song in playlist_data:
song_hash = song['hash']
difficulty = song['difficulties'][0]['name']
logging.info(f"Song added: Hash={song_hash}, Difficulty={difficulty}")
logging.info(f"Total songs added to playlist from BeatLeader accuracy gaps within star range {star_min} to {star_max}: {len(playlist_data)}")
save_history(history)
playlist_title = f"beatleader_accgraph_star_{star_level}-{new_count:02d}"
return playlist_data, playlist_title