summaryrefslogtreecommitdiff
path: root/game/shared/tf/tf_matchmaking_scoring.h
blob: f637148ec3ed9856dbe4347fc65242b467f60f9f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//========= Copyright Valve Corporation, All rights reserved. ============//
#include "tf_quickplay_shared.h"

//
// NOTE: This actually declares global variables and is intended to
// only be included ONCE on the client, and ONCE on the GC.
//

#ifdef GC
	#define TF2SCORECONVAR(name, defaultval, desc) GCConVar name(#name, defaultval, FCVAR_REPLICATED, desc)
#else
	#define TF2SCORECONVAR(name, defaultval, desc) ConVar name(#name, defaultval, FCVAR_NONE, desc)
#endif

TF2SCORECONVAR(tf_matchmaking_numbers_serverfull_headroom, "1", "Scoring will consider the server 'full' when this many slots are available" );
TF2SCORECONVAR(tf_matchmaking_numbers_valve_bonus_hrs_a, "8.00", "Valve server scoring bonus: hours played A" );
TF2SCORECONVAR(tf_matchmaking_numbers_valve_bonus_pts_a, "0.30", "Valve server scoring bonus: bonus points A" );
TF2SCORECONVAR(tf_matchmaking_numbers_valve_bonus_hrs_b, "16.00", "Valve server scoring bonus: hours played B" );
TF2SCORECONVAR(tf_matchmaking_numbers_valve_bonus_pts_b, "0.00", "Valve server scoring bonus: bonus points B" );
TF2SCORECONVAR(tf_matchmaking_numbers_increase_maxplayers_penalty, "0.50", "Max scoring penalty to servers that have increased the max number of players" );
TF2SCORECONVAR(tf_matchmaking_retry_cooldown_seconds, "300", "Time to remember quickplay join attempt, and apply scoring penalty to rejoin the same server" );
TF2SCORECONVAR(tf_matchmaking_retry_max_penalty, "1.0", "Max scoring penalty to rejoin a server previously matched.  (Decays linearly over the cooldown period)" );
TF2SCORECONVAR(tf_matchmaking_noob_map_score_boost, "0.75", "Boost added for quick-plaay scoring purposes if you are a noob and the map is considered noob-friendly" );
TF2SCORECONVAR(tf_matchmaking_noob_hours_played, "8.0", "Number of hours played to determine 'noob' status for quickplay scoring purposes" );

TF2SCORECONVAR(tf_matchmaking_ping_a,        "50.0f", "Quickplay scoring ping time data point A" );
TF2SCORECONVAR(tf_matchmaking_ping_a_score,    "0.9", "Quickplay scoring ping score data point A" );
TF2SCORECONVAR(tf_matchmaking_ping_b,       "150.0f", "Quickplay scoring ping time data point B" );
TF2SCORECONVAR(tf_matchmaking_ping_b_score,    "0.0", "Quickplay scoring ping score data point B" );
TF2SCORECONVAR(tf_matchmaking_ping_c,       "300.0f", "Quickplay scoring ping time data point C" );
TF2SCORECONVAR(tf_matchmaking_ping_c_score,   "-1.0", "Quickplay scoring ping score data point C" );

TF2SCORECONVAR(tf_matchmaking_goodenough_score_start, "8.5", "Good enough score at start of search" );
TF2SCORECONVAR(tf_matchmaking_goodenough_count_start,  "20", "Good enough count at start of search" );
TF2SCORECONVAR(tf_matchmaking_goodenough_score_end,   "7.0", "Good enough score at end of search" );
TF2SCORECONVAR(tf_matchmaking_goodenough_count_end,     "5", "Good enough count at end of search" );

TF2SCORECONVAR( tf_mm_options_bonus, "0.5", "Scoring bonus when approaching tobor rating." );
TF2SCORECONVAR( tf_mm_options_penalty, "-0.25", "Scoring penalty when options score is too far outside an acceptable range." );

TF2SCORECONVAR( tf_matchmaking_server_player_count_score, "1.5", "Maximum score when server is at/near optimal player count." );

//ConVar tf_matchmaking_goodenough_hi_score_start( "tf_matchmaking_goodenough_hi_score_start", "6.0", FCVAR_NONE );
//ConVar tf_matchmaking_goodenough_hi_count_start( "tf_matchmaking_goodenough_hi_count_start", "5", FCVAR_NONE );
//ConVar tf_matchmaking_goodenough_hi_score_end( "tf_matchmaking_goodenough_hi_score_end", "3.0", FCVAR_NONE );
//ConVar tf_matchmaking_goodenough_hi_count_end( "tf_matchmaking_goodenough_hi_count_end", "2", FCVAR_NONE );

ConVar tf_matchmaking_max_search_time( "tf_matchmaking_max_search_time", "45", FCVAR_NONE );

#undef TF2SCORECONVAR

static inline float lerp( float inA, float outA, float inB, float outB, float x )
{
	Assert( inA != inB );
	return outA + ( outB - outA ) * ( x - inA ) / ( inB - inA );
}

struct TF2ScoringNumbers_t
{

	//
	// If we do further experiments, we should make distinct enum values, so we can easily
	// compare the stats on the backend
	//
	enum ExperimentGroup_t
	{
		k_ExperimentGroup_None, // no experiment active

		//
		// Experiment 1
		//
		k_ExperimentGroup_Experiment1_Control = 1,
		k_ExperimentGroup_Experiment1_ValveBias = 2,
		k_ExperimentGroup_Experiment1_ValveBiasInactive = 3,
		k_ExperimentGroup_Experiment1_CommunityBias = 4,
		k_ExperimentGroup_Experiment1_CommunityBiasInactive = 5,
	};

	ExperimentGroup_t m_eExperimentGroup;

	TF2ScoringNumbers_t( CSteamID whosAsking )
	{
		m_eExperimentGroup = k_ExperimentGroup_None;

		//
		// Assign experiment group for experiment 1
		//
		switch ( whosAsking.GetAccountID() & 3 )
		{
			case 0:
				// 80% chance to adjust behaviour
				if ( RandomFloat( 0.0f, 1.0f) < 0.8f )
				{
					m_eExperimentGroup = k_ExperimentGroup_Experiment1_ValveBias;
				}
				else
				{
					m_eExperimentGroup = k_ExperimentGroup_Experiment1_ValveBiasInactive;
				}
				break;

			case 2:
				// 80% chance to adjust behaviour
				if ( RandomFloat( 0.0f, 1.0f) < 0.8f )
				{
					m_eExperimentGroup = k_ExperimentGroup_Experiment1_CommunityBias;
				}
				else
				{
					m_eExperimentGroup = k_ExperimentGroup_Experiment1_CommunityBiasInactive;
				}
				break;

			default:
				m_eExperimentGroup = k_ExperimentGroup_Experiment1_Control;
				break;
		}
	}
};

float QuickplayCalculateServerScore( int numHumans, int numBots, int maxPlayers, int nNumInSearchParty )
{
	Assert( nNumInSearchParty > 0 );

	// Safety check against a degenerate case with invalid max number of players.
	// Protects against some bad math below
	if ( maxPlayers < kTFQuickPlayMinMaxNumberOfPlayers )
	{
		return -100.0f;
	}
	if ( maxPlayers > kTFQuickPlayMaxPlayers )
	{
		// Server should have been filtered, but in case we get here...
		maxPlayers = kTFQuickPlayMaxPlayers;
	}

	float score = 0.0;

	// Check for completely full server
	int newNumHumans = numHumans + nNumInSearchParty;
	int newNumTotalPlayers = newNumHumans + numBots;
	if ( newNumTotalPlayers + tf_matchmaking_numbers_serverfull_headroom.GetInt() > maxPlayers )
	{
		// Server full!  Huge penalty!
		score += -100.0f;
	}
	else
	{
		// Data points for piecewise linear interpolation.
		// First point is implied: empty server is a score of zero.
		//
		// Then we increase up to point A
		int playerCountA = maxPlayers / 3;
		float scoreA = 0.20f;

		// Next data point is when the score peaks at 100%
		int idealPlayerCount = maxPlayers * 5 / 6;

		// Finally, the last data point when the server is full.
		// This choice reflects a pretty steep dropoff.  This server
		// is already in a good state, we should begin to send players
		// to other servers so that they can start to fill up, and reduce
		// the race condition with players trying to join nearly full
		// servers and being too late and getting rejected.
		float scoreFull = scoreA;
		float flMaxScore = Max( tf_matchmaking_server_player_count_score.GetFloat(), 0.1f );

		// Do the piecewise linear interpolation
		if ( newNumHumans <= playerCountA )
		{
			score += lerp( 0, 0.0f, playerCountA, scoreA, float( newNumHumans ) );
		}
		else if ( newNumHumans <= idealPlayerCount )
		{
			// Interpolate from point A up to 100%
			score += lerp( float( playerCountA ), scoreA, idealPlayerCount, flMaxScore, float( newNumHumans ) );
		}
		else
		{
			// Greater than ideal.  Interpolate back down to the score when the server is full
			score += lerp( idealPlayerCount, flMaxScore, maxPlayers, scoreFull, float( newNumHumans ) );
		}
	}

// Don't apply a penalty anymore.  Instead, we just let players express their preference
//	// Give a penalty for servers that increase the max player
//	// number above the ideal.
//	if ( maxPlayers > kTFQuickPlayIdealMaxNumberOfPlayers )
//	{
//		// Max penalty, if they increased it up all the way to
//		// kTFQuickPlayMaxPlayers.  (Above this, we reject them completely)
//		int nExcessPlayers = maxPlayers - kTFQuickPlayIdealMaxNumberOfPlayers;
//		const int kMaxExcessPlayers = kTFQuickPlayMaxPlayers - kTFQuickPlayIdealMaxNumberOfPlayers;
//		float penalty = tf_matchmaking_numbers_increase_maxplayers_penalty.GetFloat() * (float)nExcessPlayers / (float)kMaxExcessPlayers;
//		score -= penalty;
//	}

	//// being tagged as quickplay is roughly the same weight as best ping and best ratio of player numbers
	//if ( bHasQuickplayTag )
	//{
	//	item.score += 2.0f;
	//}

	return score;
}