TFT Using Genetic Algorithm
Teamfight Tactics (TFT) is a strategy-based autobattler: a round-based PvP game where eight players create teams that battle automatically on their behalf. The game’s complexity lies in its layered decision-making: managing your economy to purchase units, scouting opponents to adjust positioning, and crafting optimal items to enhance team performance. Among these, determining the most effective team composition is arguably the most critical—and most challenging—aspect of the game.
Traditionally, players discover optimal team compositions, known as the “meta,” through iterative gameplay and aggregated statistical analysis. High-ranking players naturally gravitate toward these meta compositions, which are usually identified by their superior win rates. However, TFT adds another layer of complexity: a finite shared champion pool. When multiple players pursue the same meta team, competition for key units often results in shortages, forcing players to pivot strategies mid-game. This dynamic makes mastering TFT not just a matter of skill but also of adaptability.
In Set 11, TFT introduced the Exalted trait, a trait that when activated, provides a substantial damage boost to the entire team. Unlike most traits, champions that possessed the exalted traits are random between games, which introduced an element of unpredictability that discouraged its use in favor of more stable meta compositions. So dDespite its immense potential, Exalted compositions were underutilized due to their inconsistency and difficulty integrating with established meta teams.
However, after attending a class where we discussed the genetic algorithm, I wondered if it was possible to apply this technique to TFT. The genetic algorithm is particularly well-suited to problems that have complex solution spaces, and determining the best team composition based around a particular trait seemed to fit that bill. By encoding team compositions as candidate solutions and designing a fitness function to evaluate their effectiveness, I could simulate the evolutionary processes to identify the optimal combinations.
Key TFT Terms and Mechanics
This section provides an overview of some terms and mechanics used in TFT that may be helpful to better understand this project.
Champions
What They Are: Champions are the units players deploy on their boards to battle automatically each round. Each champion has a unique combination of traits, stats (e.g., health, damage, and range), and abilities.
Cost and Rarity: Champions range in cost from 1 to 5 gold. Higher-cost champions are generally stronger and harder to acquire, as they appear less frequently in the shop.
Champion Pool: TFT features a finite, shared pool of champions. If many players buy the same champion, it becomes scarce for everyone else.
Traits
Definition: Traits are classifications assigned to champions (e.g., “Bruiser,” “Exalted,” “Warden”) that grant bonuses when a certain number of champions with the same trait are on the board.
Synergy Levels: Traits have thresholds for activation. For example, the Bruiser trait may grant health bonuses at 2, 4, and 6 champions. Achieving higher thresholds generally provides stronger effects.
Vertical vs. Horizontal Traits:
- Vertical Traits: Focus on maximizing a single trait to the highest synergy level (e.g., 6 Bruisers).
- Horizontal Traits: Spread across multiple traits for more flexible compositions (e.g., activating 2 Bruisers, 2 Wardens, and 2 Rangers).
Team Composition
What It Means: A team composition, or “comp,” is the set of champions a player fields. The goal is to build a team with strong synergies and complementary roles.
Frontline vs. Backline:
- Frontline: Tanky champions placed at the front to absorb damage.
- Backline: Damage-dealing champions placed at the back to attack safely.
Carry Unit: A designated champion (often with high damage output) around which the team is built. Supporting the carry’s traits and survivability is critical.
Economy
Gold: The in-game currency used to buy champions, reroll the shop, and level up.
Income Sources:
- Base Income: Gold received every round, regardless of performance.
- Win/Loss Streaks: Bonus gold for consecutive wins or losses.
- Interest: Earned by saving gold, with 1 gold rewarded for every 10 gold saved (up to 5 gold per round).
Spending Strategies:
- Rerolling: Spending gold to refresh the shop in search of specific champions.
- Leveling Up: Spending gold to increase your level, unlocking higher chances for rare champions and more slots for units.
Champion Pool Mechanics
Shared Pool: All players draw champions from a single shared pool. For example, if multiple players are buying Annie, there will be fewer Annies available for everyone.
Contesting: When multiple players chase the same composition, competition for key champions can force pivots or alternative strategies.
Combat
Automatic Battles: Once the planning phase ends, the selected champions automatically fight against an opponent’s team. Players cannot control their units during combat.
Round Outcomes:
- Winning a round deals damage to the opponent based on the surviving units and the player's level.
- Losing a round results in taking damage based on the opponent’s surviving units.
Knockout Mechanics: Players start with a fixed amount of health and are eliminated when their health reaches zero.
Items
Definition: Items are equippable bonuses that enhance a champion’s stats or grant special effects. They are critical for maximizing a team’s effectiveness.
Crafting: Items are crafted by combining two basic components (e.g., a Giant's Belt + Needlessly Large Rod = Morellonomicon).
Itemization: Strategically assigning items to champions based on their roles. For instance:
- Damage items for carries.
- Defensive items for frontline tanks.
Exalted Trait (Set 11 Specific)
Overview: The Exalted trait provides a team-wide damage boost when a certain number of Exalted champions are on the board.
Unique Mechanic: Unlike most traits, the champions with the Exalted trait change each game, making it unpredictable and challenging to plan around. There are 5-6 random units that become Exalted each game. Since the randomness can often result in many poor quality Exalted units, I target utilizing only 3 Exalted units to activate the trait to ensure a better team quality.
Meta
Definition: The “meta” refers to the most effective strategies, team compositions, and item builds based on statistical analysis and high-level play.
Adaptation: Players often tailor their strategies to counter the current meta or find less-contested compositions to exploit weaknesses in popular builds.
How the Genetic Algorithm Works
A genetic algorithm is a search and optimization method inspired by the process of natural selection. It starts with a population of candidate solutions, each represented by some form of encoded “genes.” These solutions could, for example, be bit strings, numeric arrays, or even symbolic expressions—anything that can be manipulated similarly to biological genes. The basic idea is to evolve this population over multiple generations, gradually honing in on better-performing candidates with respect to a given “fitness” measure.
In each generation, the genetic algorithm applies three main operators: selection, crossover, and mutation. Selection favors candidates with higher fitness scores, giving them a greater chance of passing their “genes” to the next generation. Crossover merges parts of two parent solutions to produce offspring—some traits from one parent, some from the other—thereby creating new solutions with potentially better characteristics. Mutation introduces small, random changes in the genes of some offspring. Although most mutations might be neutral or detrimental, occasional beneficial mutations help prevent the population from getting stuck in poor solutions.
Through these iterative steps, the genetic algorithm explores and exploits the solution space. And while genetics algorithms do not guarantee finding a global optimum for every problem, their flexibility and robustness make them especially useful in challenging optimization tasks, such as making teams for TFT.
What Makes a Good TFT Team?
In order to apply the genetic algorithm to TFT, it is important to be able to evaluate each team and score it. This score, or fitness, is used by the fitness
function. This function assesses team compositions based on multiple factors that align with TFT's strategic mechanics. Based on my experience as a player and some experimentation, here’s what I decided what makes up a strong team:
Active Traits and Synergies
- Trait Activation: Teams are rewarded for activating powerful traits, particularly the Exalted trait, which provides a significant damage boost.
- Scaling with Count: Traits with thresholds (e.g., activating at 2, 4, or 6 units) are evaluated to ensure the team composition effectively leverages these thresholds.
- Penalties for Non-Activated Traits: Teams are penalized for including champions whose traits are not activated, discouraging wasted potential.
Team Balance
- Frontline vs. Backline: The fitness function considers the balance between tanky frontline units and damage-dealing backline units. A balanced range of champions is critical to a team’s survivability and damage output.
- Tank Quality: Additional weight is given to high-cost tanks or champions with valuable tank traits like Bruiser or Warden in order to promote a stronger defensive synergy. I also added weights to make good tanks units that do not have the tank traits viable in the team evaluation. Units that met this criteria were added due to my gameplay experience with those units.
Champion Costs
- High-Cost Units: Teams are rewarded for including high-cost champions, as these are generally more powerful and offer better value for their gold investment.
- Penalties for Overloading Low-Cost Units: To discourage spamming low-cost champions, the function deducts points for teams with an abundance of cheap units.
Exalted Champions
- Maximizing Exalted Traits: Since Exalted is the focal trait, teams with the specified number of Exalted champions are heavily rewarded. Missing or overusing Exalted units leads to penalties.
- Trait Interactions: The algorithm ensures that Exalted champions synergize well with other traits and the carry unit (if specified).
Carry Synergy
If a specific carry champion is designated, the fitness function ensures the carry’s traits are well-supported by the team composition. Synergies with the carry are prioritized, and penalties are applied if the carry’s traits are underutilized. When playing a flexible board, players will often try to pick a good carry that is not very contested. This is done by scouting other player boards so I included an option to manually select a unit that the algorithm will then try to build a team composition around.
Range and Positioning
The algorithm evaluates the average range of the team to ensure a mix of melee and ranged champions, avoiding compositions that are too one-dimensional.
Applying the Algorithm to TFT
The algorithm evolves teams over multiple generations. It starts with an initial population of randomly generated teams and iteratively improves them through selection, crossover, and mutation. This process repeats for the specified number of generations and the algorithm maintains the best teams based on fitness and outputs the top-ranked teams after all generations. This method is implemented in the natural_selection
function.
def natural_selection(self, exalt_ids_=[], exclude_ids_=[], carry=None):
POPULATION_SIZE = self.POPULATION_SIZE
GENERATIONS = self.GENERATIONS
TEAM_SIZE = self.TEAM_SIZE
NUM_EXALTED = self.NUM_EXALTED
CARRY = None
all_generations = []
if carry not in self.unit_to_id:
carry = None
if carry is not None:
carry_id = self.unit_to_id[carry]
TEAM_SIZE -= 1
CARRY = self.set_11_champs_dict[carry_id]
if carry_id in exalt_ids_:
NUM_EXALTED -= 1
TEAM_SIZE += 1
exalt_ids_ = list(set(exalt_ids_).difference(set([carry_id])))
population = [self.generate_random_team(exalt_ids_, exclude_ids_, TEAM_SIZE, NUM_EXALTED, CARRY) for _ in range(POPULATION_SIZE)]
for generation in range(GENERATIONS):
new_population = []
while len(new_population) < POPULATION_SIZE:
parent1 = self.select_parent(population, CARRY)
parent2 = self.select_parent(population, CARRY)
child1, child2 = self.crossover(parent1, parent2, TEAM_SIZE)
child1 = self.mutate(child1, generation, exalt_ids_, exclude_ids_, TEAM_SIZE, NUM_EXALTED, CARRY)
child2 = self.mutate(child2, generation, exalt_ids_, exclude_ids_, TEAM_SIZE, NUM_EXALTED, CARRY)
if carry is not None:
self.ensure_carry(child1, CARRY)
self.ensure_carry(child2, CARRY)
new_population.extend([child1, child2])
population = new_population[:self.POPULATION_SIZE]
population.sort(key=lambda x: self.fitness(x, CARRY), reverse=True)
all_generations += population
return all_generations
Selection
Parents are chosen using a tournament selection method, where a small subset of the population is sampled, and the team with the highest fitness is selected. This is implemented in the select_parent
function.
def select_parent(self, population, carry):
tournament = random.sample(population, 10)
tournament.sort(key=lambda x: self.fitness(x, carry), reverse=True)
idx_tournament = np.random.choice(np.arange(3))
return tournament[idx_tournament]
Crossover
Crossover combines two parent teams by swapping parts of their compositions at a randomly selected point, creating two offspring teams. The default team size is set to 8 since that the most optimal level to play this composition due to tempo and amount of traits that can be fit into the team composition. This is implemented in the crossover
function.
def crossover(self, parent1, parent2, team_size = 8):
crossover_point = random.randint(1, team_size - 1)
child1 = parent1[:crossover_point] + parent2[crossover_point:]
child2 = parent2[:crossover_point] + parent1[crossover_point:]
return (child1, child2)
Mutation
Finally, it is important to introduce some diversity by randomly replacing champions in a team. The mutation rate decreases over generations to favor refinement as the population converges. Furthermore, I split each team into two parts, Exalted and Non-Exalted, and perform mutations on each part separately. This allows for experimentation of teams with different Exalted units as well as different non-Exalted units, ensuring that the resulting mutation maintains the Exalted trait active. This is implemented in the mutate
function.
def mutate(self, team_, rate, exalt_ids_=[], exclude_ids_=[], team_size_=8, num_exalted=3, CARRY=None):
team_, _ = self.uniquify_team(team_)
exc_ids = exalt_ids_ + exclude_ids_ + [self.unit_to_id[t["name"]] for t in team_]
if len(team_) < team_size_:
team_ += self.generate_random_team(team_size=team_size_ - len(team_), exclude_ids=exc_ids, num_exalted=0)
team_names = set([ch["name"] for ch in team_])
exalts = [self.set_11_champs_dict[self.unit_to_id[name]] for name in set(self.convert_id_to_name_list(exalt_ids_)).intersection(team_names)]
non_exalts = [self.set_11_champs_dict[self.unit_to_id[name]] for name in team_names.difference(set(self.convert_id_to_name_list(exalt_ids_)))]
mutation_rate = max(0.8 / (rate / 100 + 1), 0.2)
if random.random() < mutation_rate: # Mutation chance
# weighted random choice. 70% to mutate one, 20% to mutate 2, 10% to mutate 1
mutate_size = np.random.choice(np.arange(1, 4), p=[0.7, 0.2, 0.1])
mutate_indexes = np.random.choice(
np.arange(len(non_exalts)),
size=min(mutate_size, len(non_exalts)),
replace=False)
for mutate_index in mutate_indexes:
non_exalts[mutate_index] = self.generate_random_team(
team_size=1, exclude_ids=exc_ids, num_exalted=0)[0]
exalts = self.generate_random_team(
team_size=len(exalts),
exalted_ids=exalt_ids_,
exclude_ids=exclude_ids_,
num_exalted=len(exalts))
team_ = exalts + non_exalts
if len(team_) < team_size_:
team_ += self.generate_random_team(
team_size=team_size_ - len(team_),
exclude_ids=exc_ids,
num_exalted=0)
return team_