AI-система персонализации игрового опыта
Персонализация в играх — это не только рекомендации предметов. AI адаптирует сложность в реальном времени, генерирует нарратив под стиль игрока, подбирает соперников одного уровня и создаёт уникальные события для каждого пользователя. Цель: максимизировать time-in-game и lifetime value.
Динамическая адаптация сложности (DDA)
import numpy as np
from collections import deque
from dataclasses import dataclass
from typing import Optional
@dataclass
class GameSession:
player_id: str
skill_level: float # 0-1
current_difficulty: float # 0-1
recent_outcomes: deque # True=win, False=lose
frustration_score: float
boredom_score: float
class DynamicDifficultyAdjuster:
"""
Flow theory: игрок в состоянии потока между скукой и фрустрацией.
Target win rate: 65-75% для оптимального engagement.
"""
TARGET_WIN_RATE = 0.70
ADJUSTMENT_SPEED = 0.05 # Изменение сложности за шаг
WINDOW_SIZE = 10 # Последние N исходов для оценки
def update_difficulty(self, session: GameSession,
last_outcome: bool,
time_to_complete_seconds: float,
health_remaining_pct: float = 1.0) -> float:
"""Обновление сложности после каждого энкаунтера"""
session.recent_outcomes.append(last_outcome)
if len(session.recent_outcomes) < 3:
return session.current_difficulty
recent_win_rate = sum(session.recent_outcomes) / len(session.recent_outcomes)
# Фрустрация: серия поражений + низкое здоровье
if not last_outcome and health_remaining_pct < 0.1:
session.frustration_score = min(1.0, session.frustration_score + 0.2)
else:
session.frustration_score = max(0.0, session.frustration_score - 0.05)
# Скука: слишком быстрое прохождение + высокое здоровье
if last_outcome and time_to_complete_seconds < 30 and health_remaining_pct > 0.8:
session.boredom_score = min(1.0, session.boredom_score + 0.15)
else:
session.boredom_score = max(0.0, session.boredom_score - 0.05)
# Корректировка сложности
new_difficulty = session.current_difficulty
if session.frustration_score > 0.6:
new_difficulty -= self.ADJUSTMENT_SPEED * 1.5 # Быстрее снижаем
elif session.boredom_score > 0.6:
new_difficulty += self.ADJUSTMENT_SPEED * 1.5 # Быстрее повышаем
elif recent_win_rate > self.TARGET_WIN_RATE + 0.1:
new_difficulty += self.ADJUSTMENT_SPEED
elif recent_win_rate < self.TARGET_WIN_RATE - 0.1:
new_difficulty -= self.ADJUSTMENT_SPEED
session.current_difficulty = float(np.clip(new_difficulty, 0.1, 1.0))
return session.current_difficulty
def scale_enemy_parameters(self, base_enemy: dict,
difficulty: float) -> dict:
"""Масштабирование параметров врага под сложность"""
scale_factor = 0.5 + difficulty * 1.0 # 0.1 → 0.6x, 1.0 → 1.5x
return {
'hp': int(base_enemy['hp'] * scale_factor),
'damage': round(base_enemy['damage'] * scale_factor, 2),
'speed': round(base_enemy['speed'] * (0.8 + difficulty * 0.4), 2),
'accuracy': min(0.95, base_enemy['accuracy'] * scale_factor),
'ai_reaction_ms': int(base_enemy['ai_reaction_ms'] / scale_factor),
'loot_bonus_pct': int(difficulty * 50) # Больше наград за высокую сложность
}
class MatchmakingSystem:
"""Система подбора соперников и команд"""
def __init__(self):
self.rating_k_factor = 32 # Elo K-factor
def compute_elo_rating(self, player_rating: float,
opponent_rating: float,
outcome: float) -> float:
"""Обновление Elo рейтинга"""
expected = 1 / (1 + 10 ** ((opponent_rating - player_rating) / 400))
return player_rating + self.rating_k_factor * (outcome - expected)
def find_match(self, player_id: str,
player_rating: float,
player_latency_ms: int,
available_players: list[dict],
max_wait_seconds: int = 60) -> Optional[list[dict]]:
"""
Балансировка: рейтинг + пинг + время ожидания.
Расширяем коридор рейтинга с течением времени ожидания.
"""
wait_factor = min(max_wait_seconds, 30) / 30 # 0-1
rating_window = 100 + wait_factor * 200 # 100-300 Elo
candidates = []
for p in available_players:
if p['player_id'] == player_id:
continue
rating_diff = abs(p['rating'] - player_rating)
if rating_diff > rating_window:
continue
# Пинг: предпочтительно < 80ms разница
latency_diff = abs(p.get('latency_ms', 50) - player_latency_ms)
latency_score = max(0, 1.0 - latency_diff / 150)
rating_score = 1.0 - rating_diff / rating_window
combined_score = rating_score * 0.7 + latency_score * 0.3
candidates.append({**p, 'match_score': combined_score})
if not candidates:
return None
# Возвращаем лучшего кандидата
return sorted(candidates, key=lambda x: -x['match_score'])[:1]
class PersonalizedEventGenerator:
"""Генерация персональных внутриигровых событий"""
PLAYER_MOTIVATIONS = {
'explorer': ['discovery_event', 'hidden_area_unlock', 'lore_reveal'],
'achiever': ['challenge_milestone', 'rare_achievement', 'collection_complete'],
'socializer': ['guild_event', 'co-op_mission', 'friend_challenge'],
'competitor': ['ranked_event', 'leaderboard_challenge', 'pvp_tournament'],
}
def generate_personal_event(self, player: dict,
days_since_last_event: int,
player_motivation: str) -> dict:
"""Персональное событие под мотивационный профиль"""
# Частота: максимум 1 событие в 3 дня
if days_since_last_event < 3:
return {'event': None, 'reason': 'Too soon since last event'}
event_types = self.PLAYER_MOTIVATIONS.get(
player_motivation, self.PLAYER_MOTIVATIONS['achiever']
)
event_type = np.random.choice(event_types)
# Параметры события от профиля игрока
event_config = self._configure_event(event_type, player)
return {
'event_type': event_type,
'config': event_config,
'expires_in_hours': 48,
'notification_channel': 'push_and_in_game',
'reward_scale': min(2.0, 1.0 + player.get('days_active', 0) / 100)
}
def _configure_event(self, event_type: str, player: dict) -> dict:
configs = {
'challenge_milestone': {
'target': player.get('current_level', 1) * 10,
'reward': 'unique_badge',
'difficulty': 'hard'
},
'pvp_tournament': {
'bracket': 'skill_based',
'rounds': 3,
'prize_pool': '1000 gems'
},
'hidden_area_unlock': {
'area_tier': min(5, player.get('current_level', 1) // 10 + 1),
'hint_provided': True
}
}
return configs.get(event_type, {'type': event_type})
DDA в крупных мобильных играх (данные Supercell, King) показывает: оптимальный win rate 65-75% максимизирует D7 retention. При win rate ниже 55% — frustation churn, выше 85% — boredom churn. Персонализированные события увеличивают DAU/MAU ratio на 8-15% при корректной сегментации мотиваций.







