[Enhance] World rank score mechanism

- Adjust world rank mechanism to be closer to the official one.

Note: You need to refresh rating in web admin backend after updating, and the users need to get a better or new score to refresh world rank.
This commit is contained in:
Lost-MSth
2024-06-14 16:06:26 +08:00
parent 338f6579aa
commit 2a08c9cd14
11 changed files with 137 additions and 30 deletions

View File

@@ -12,7 +12,7 @@ class Config:
SONG_FILE_HASH_PRE_CALCULATE = True
GAME_API_PREFIX = '/hanami/29' # str | list[str]
GAME_API_PREFIX = '/natsugakuru/30' # str | list[str]
OLD_GAME_API_PREFIX = [] # str | list[str]
ALLOW_APPVERSION = [] # list[str]

View File

@@ -1,7 +1,7 @@
from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.3.12'
ARCAEA_DATABASE_VERSION = 'v2.11.3.11'
ARCAEA_SERVER_VERSION = 'v2.11.3.13'
ARCAEA_DATABASE_VERSION = 'v2.11.3.13'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'

View File

@@ -27,6 +27,7 @@ class BaseOperation:
class RefreshAllScoreRating(BaseOperation):
'''
刷新所有成绩的评分
包括 score_v2
'''
_name = 'refresh_all_score_rating'
@@ -44,11 +45,11 @@ class RefreshAllScoreRating(BaseOperation):
for i in x:
for j in range(0, 4):
defnum = -10 # 没在库里的全部当做定数-10
defnum = -10 # 没在库里的全部当做定数 -10
if i[j+1] is not None and i[j+1] > 0:
defnum = float(i[j+1]) / 10
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
c.execute('''select user_id, score, shiny_perfect_count, perfect_count, near_count, miss_count from best_score where song_id=:a and difficulty=:b''', {
'a': i[0], 'b': j})
y = c.fetchall()
values = []
@@ -56,10 +57,12 @@ class RefreshAllScoreRating(BaseOperation):
for k in y:
ptt = Score.calculate_rating(defnum, k[1])
ptt = max(ptt, 0)
values.append((ptt,))
score_v2 = Score.calculate_score_v2(
defnum, k[2], k[3], k[4], k[5])
values.append((ptt, score_v2,))
where_values.append((k[0], i[0], j))
if values:
Sql(c).update_many('best_score', ['rating'], values, [
Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [
'user_id', 'song_id', 'difficulty'], where_values)
@@ -133,11 +136,16 @@ class SaveUpdateScore(BaseOperation):
new_scores = []
for i in save.scores_data:
rating = 0
score_v2 = 0
if i['song_id'] in song_chart_const:
rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
defnum = song_chart_const[i['song_id']
][i['difficulty']] / 10
rating = Score.calculate_rating(defnum, i['score'])
rating = max(rating, 0)
score_v2 = Score.calculate_score_v2(
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state:
clear_type = clear_state[y]
@@ -145,10 +153,10 @@ class SaveUpdateScore(BaseOperation):
clear_type = 0
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2))
c.executemany(
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
def _all_update(self):
with Connect() as c:
@@ -169,11 +177,16 @@ class SaveUpdateScore(BaseOperation):
new_scores = []
for i in save.scores_data:
rating = 0
score_v2 = 0
if i['song_id'] in song_chart_const:
rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
defnum = song_chart_const[i['song_id']
][i['difficulty']] / 10
rating = Score.calculate_rating(defnum, i['score'])
rating = max(rating, 0)
score_v2 = Score.calculate_score_v2(
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state:
clear_type = clear_state[y]
@@ -181,10 +194,10 @@ class SaveUpdateScore(BaseOperation):
clear_type = 0
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2))
c.executemany(
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
class UnlockUserItem(BaseOperation):

View File

@@ -30,6 +30,7 @@ class Score:
self.best_clear_type: int = None
self.clear_type: int = None
self.rating: float = None
self.score_v2: float = None # for `world_rank_score` of global rank
def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int):
self.score = int(score) if score is not None else 0
@@ -124,12 +125,31 @@ class Score:
return ptt
@staticmethod
def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int) -> float:
# 计算score_v2 refer: https://www.bilibili.com/video/BV1ys421u7BY
# 谱面定数小于等于 0 视为 unranked返回值会为 0
if not defnum or defnum <= 0:
return 0
all_note = perfect_count + near_count + miss_count
if all_note == 0:
return 0
shiny_ratio = shiny_perfect_count / all_note
score_ratio = (perfect_count + near_count/2) / \
all_note + shiny_perfect_count / 10000000
acc_rating = max(0, min(shiny_ratio - 0.9, 0.095)) / 9.5 * 25
score_rating = max(0, min(score_ratio - 0.99, 0.01)) * 75
return defnum * (acc_rating + score_rating)
def get_rating_by_calc(self) -> float:
# 通过计算得到本成绩的rating
# 通过计算得到本成绩的 rating & score_v2
if not self.song.defnum:
self.song.c = self.c
self.song.select()
self.rating = self.calculate_rating(self.song.chart_const, self.score)
self.score_v2 = self.calculate_score_v2(
self.song.chart_const, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count)
return self.rating
def to_dict(self) -> dict:
@@ -181,6 +201,7 @@ class UserScore(Score):
self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12])
self.best_clear_type = int(x[11])
self.rating = float(x[13])
self.score_v2 = float(x[14])
return self
@@ -430,8 +451,9 @@ class UserPlay(UserScore):
x = self.c.fetchone()
if not x:
self.new_best_protect_flag = True # 初见保护
self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating})
self.c.execute('''insert into best_score values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(self.user.user_id, self.song.song_id, self.song.difficulty, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count,
self.health, self.modifier, self.time_played, self.clear_type, self.clear_type, self.rating, self.score_v2))
self.user.update_global_rank()
else:
self.new_best_protect_flag = False
@@ -440,8 +462,8 @@ class UserPlay(UserScore):
'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty})
if self.score >= int(x[0]): # best成绩更新
self.new_best_protect_flag = True
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played})
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m, score_v2 = :n where user_id = :a and song_id = :b and difficulty = :c ''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played, 'n': self.score_v2})
self.user.update_global_rank()
self.ptt = Potential(self.c, self.user)

View File

@@ -351,7 +351,8 @@ class Sql:
class DatabaseMigrator:
SPECIAL_UPDATE_VERSION = {
'2.11.3.11': '_version_2_11_3_11'
'2.11.3.11': '_version_2_11_3_11',
'2.11.3.13': '_version_2_11_3.13'
}
def __init__(self, c1_path: str, c2_path: str) -> None:
@@ -463,6 +464,12 @@ class DatabaseMigrator:
self.c2.executemany(
'''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list)
def _version_2_11_3_13(self):
'''
2.11.3.13 版本特殊更新world_rank_score 机制调整,需清空用户分数
'''
self.c1.execute('''update user set world_rank_score = 0''')
class LogDatabaseMigrator:

View File

@@ -528,7 +528,11 @@ class UserInfo(User):
'world_mode_locked_end_ts': self.world_mode_locked_end_ts,
'locked_char_ids': [], # [1]
'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(),
'pick_ticket': self.pick_ticket
'pick_ticket': self.pick_ticket,
# 'custom_banner': 'online_banner_2024_06',
# 'subscription_multiplier': 114,
# 'memory_boost_ticket': 5,
}
def from_list(self, x: list) -> 'UserInfo':
@@ -648,8 +652,8 @@ class UserInfo(User):
'''用户世界排名如果超过设定最大值返回0'''
if self.world_rank_score is None:
self.select_user_one_column('world_rank_score', 0)
if self.world_rank_score is None:
return 0
if not self.world_rank_score:
return 0
self.c.execute(
'''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,))
@@ -665,14 +669,14 @@ class UserInfo(User):
self.c.execute(
'''
with user_scores as (
select song_id, difficulty, score from best_score where user_id = ? and difficulty in (2, 3, 4)
select song_id, difficulty, score_v2 from best_score where user_id = ? and difficulty in (2, 3, 4)
)
select sum(a) from(
select sum(score) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0)
select sum(score_v2) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0)
union
select sum(score) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0)
select sum(score_v2) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0)
union
select sum(score) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0)
select sum(score_v2) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0)
)
''',
(self.user_id,)

View File

@@ -265,6 +265,7 @@ class UserMap(Map):
r['curr_capture'] = self.curr_capture
r['is_locked'] = self.is_locked
r['user_id'] = self.user.user_id
# memory_boost_ticket
if not has_steps:
del r['steps']
if has_rewards:
@@ -668,6 +669,9 @@ class BaseWorldPlay(WorldSkillMixin):
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
'beyond_boost_gauge': self.user.beyond_boost_gauge,
# 'wpaid': 'helloworld', # world play id ???
# progress_before_sub_boost
# progress_sub_boost_amount
# subscription_multiply
}
if self.character_used.skill_id_displayed == 'skill_maya':

View File

@@ -70,7 +70,7 @@ class InitData:
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc']
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive']
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage']
world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"]

View File

@@ -1774,5 +1774,59 @@
],
"orig_price": 100,
"price": 100
},
{
"name": "backtobasics",
"items": [
{
"type": "single",
"id": "backtobasics",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "tasogare",
"items": [
{
"type": "single",
"id": "tasogare",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "beautifuldreamer",
"items": [
{
"type": "single",
"id": "beautifuldreamer",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
}
]

View File

@@ -61,7 +61,8 @@ modifier int,
time_played int,
best_clear_type int,
clear_type int,
rating real,
rating real default 0,
score_v2 real default 0,
primary key(user_id, song_id, difficulty)
);
create table if not exists user_char(user_id int,

View File

@@ -106,6 +106,8 @@ def buy_special(user_id):
x.discount_from = -1
x.discount_to = -1
x.items = [ItemFactory(c).get_item(item_id)]
# request.form['ticket_used'] == 'true'
# memory_boost_ticket: x-1
x.buy()
r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}