diff --git a/alembic/versions/1e0b04a3790e_valorsbot_model.py b/alembic/versions/1e0b04a3790e_valorsbot_model.py new file mode 100644 index 0000000..eee123b --- /dev/null +++ b/alembic/versions/1e0b04a3790e_valorsbot_model.py @@ -0,0 +1,46 @@ +"""ValorsBot model + +Revision ID: 1e0b04a3790e +Revises: 635cc8120b46 +Create Date: 2024-10-06 04:30:38.165999 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '1e0b04a3790e' +down_revision: Union[str, None] = '635cc8120b46' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('bot_settings', schema=None) as batch_op: + batch_op.drop_column('mm_maps_phase') + + with op.batch_alter_table('mm_bot_matches', schema=None) as batch_op: + batch_op.drop_column('maps_phase') + + with op.batch_alter_table('mm_bot_user_summary_stats', schema=None) as batch_op: + batch_op.add_column(sa.Column('momentum', sa.Float(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('mm_bot_user_summary_stats', schema=None) as batch_op: + batch_op.drop_column('momentum') + + with op.batch_alter_table('mm_bot_matches', schema=None) as batch_op: + batch_op.add_column(sa.Column('maps_phase', sa.BIGINT(), server_default=sa.text('0'), autoincrement=False, nullable=False)) + + with op.batch_alter_table('bot_settings', schema=None) as batch_op: + batch_op.add_column(sa.Column('mm_maps_phase', sa.SMALLINT(), autoincrement=False, nullable=False)) + + # ### end Alembic commands ### diff --git a/src/config.py b/src/config.py index 746cd2b..1f29d11 100644 --- a/src/config.py +++ b/src/config.py @@ -46,6 +46,8 @@ MATCH_PLAYER_COUNT = 10 STARTING_MMR = 900 BASE_MMR_CHANGE = 40 +MOMENTUM_CHANGE = 0.05 +MOMENTUM_RESET_FACTOR = 0.3 PLACEMENT_MATCHES = 10 REGION_TIMEZONES = { diff --git a/src/matches/functions.py b/src/matches/functions.py index 9b70f81..03a58b5 100644 --- a/src/matches/functions.py +++ b/src/matches/functions.py @@ -20,7 +20,7 @@ from typing import List import numpy as np -from config import BASE_MMR_CHANGE, STARTING_MMR +from config import BASE_MMR_CHANGE, STARTING_MMR, MOMENTUM_CHANGE, MOMENTUM_RESET_FACTOR from utils.models import MMBotMaps, MMBotUserMapPicks, Side from utils.utils import lerp @@ -71,7 +71,8 @@ def calculate_mmr_change( enemy_team_avg_mmr: int=0, win: bool=False, abandoned_count: int=0, - placements: bool=False + placements: bool=False, + momentum: float=1.0 ) -> int: kills = player_stats.get('kills', 0) deaths = player_stats.get('deaths', 0) @@ -96,6 +97,8 @@ def calculate_mmr_change( new_r = base_change * (int(win) - pr_a) new_r *= closeness new_r += kd_rate + if not abandoned_count: + new_r *= momentum return new_r def calculate_placements_mmr(user_avg_score: int, guild_avg_scores: List[int], initial_mmr: int) -> int: @@ -119,4 +122,14 @@ def calculate_placements_mmr(user_avg_score: int, guild_avg_scores: List[int], i mmr_change = lerp(lower_mmr, upper_mmr, normalized_score) break - return max(STARTING_MMR - 300, min(STARTING_MMR + 450, initial_mmr + mmr_change)) \ No newline at end of file + return max(STARTING_MMR - 300, min(STARTING_MMR + 450, initial_mmr + mmr_change)) + +def update_momentum(current_momentum, win): + if (win and current_momentum >= 1.0) or (not win and current_momentum <= 1.0): + new_momentum = current_momentum + MOMENTUM_CHANGE if win else current_momentum - MOMENTUM_CHANGE + else: + difference = current_momentum - 1.0 + reset = difference * MOMENTUM_RESET_FACTOR + new_momentum = current_momentum - reset + + return max(0.5, min(2.0, new_momentum)) \ No newline at end of file diff --git a/src/matches/match.py b/src/matches/match.py index e9bf057..5fa6134 100644 --- a/src/matches/match.py +++ b/src/matches/match.py @@ -50,7 +50,7 @@ from views.match.side_pick import ChosenSideView, SidePickView from views.match.no_server_found import NoServerFoundView from views.match.force_abandon import ForceAbandonView -from .functions import calculate_mmr_change, get_preferred_bans, get_preferred_map, get_preferred_side, calculate_placements_mmr +from .functions import calculate_mmr_change, get_preferred_bans, get_preferred_map, get_preferred_side, calculate_placements_mmr, update_momentum from .match_states import MatchState from .ranked_teams import get_teams @@ -238,6 +238,7 @@ def update_user_match_stats(self, user_stats, player_data): def update_summary_stats(self, summary_data, match_stats): return { "mmr": summary_data.mmr + match_stats['mmr_change'], + "momentum": update_momentum(summary_data.momentum, match_stats['win']), "games": summary_data.games + 1, "wins": summary_data.wins + int(match_stats['win']), "losses": summary_data.losses + int(not match_stats['win']), @@ -277,19 +278,23 @@ async def finalize_match(self, users_summary_data, team_scores, last_reply: dict ct_start = current_stats['ct_start'] win = team_scores[0] > team_scores[1] if ct_start else team_scores[1] > team_scores[0] + summary_data = users_summary_data[user_id] ally_score = team_scores[0] if ct_start else team_scores[1] enemy_score = team_scores[1] if ct_start else team_scores[0] ally_mmr = self.match.a_mmr if player.team == Team.A else self.match.b_mmr enemy_mmr = self.match.b_mmr if player.team == Team.A else self.match.a_mmr + mmr_change = calculate_mmr_change(current_stats, - ally_team_score=ally_score, enemy_team_score=enemy_score, - ally_team_avg_mmr=ally_mmr, enemy_team_avg_mmr=enemy_mmr, win=win, - placements=games_played <= PLACEMENT_MATCHES) + ally_team_score=ally_score, + enemy_team_score=enemy_score, + ally_team_avg_mmr=ally_mmr, + enemy_team_avg_mmr=enemy_mmr, win=win, + placements=games_played <= PLACEMENT_MATCHES, + momentum=summary_data.momentum) current_stats.update({"win": win, "mmr_change": mmr_change}) - summary_data = users_summary_data[user_id] new_mmr = summary_data.mmr + current_stats['mmr_change'] if games_played == PLACEMENT_MATCHES: placement_completions.append((member, new_mmr)) diff --git a/src/utils/models.py b/src/utils/models.py index 4f17f6d..de27456 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -276,6 +276,7 @@ class MMBotUserSummaryStats(Base): guild_id = Column(BigInteger, primary_key=True, nullable=False) user_id = Column(BigInteger, primary_key=True, nullable=False) mmr = Column(Float, default=900) + momentum = Column(Float, default=1) games = Column(Integer, default=0) wins = Column(Integer, default=0) losses = Column(Integer, default=0) diff --git a/src/views/match/force_abandon.py b/src/views/match/force_abandon.py index d88fae1..04754eb 100644 --- a/src/views/match/force_abandon.py +++ b/src/views/match/force_abandon.py @@ -121,10 +121,10 @@ async def cancel_callback(interaction: nextcord.Integration): cancel_button.callback = cancel_callback view.add_item(cancel_button) - cancel_button = nextcord.ui.Button( + confirm_button = nextcord.ui.Button( label="Confirm", emoji="✔️", style=nextcord.ButtonStyle.danger) - cancel_button.callback = confirm_callback - view.add_item(confirm_callback) + confirm_button.callback = confirm_callback + view.add_item(confirm_button) embed = nextcord.Embed( title="Force Abandon the missing players?",