Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working set 11 rolldown, with correct roll logic. #7

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/lvl4_1cost.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
level: 5
champs_to_buy:
- name: Caitlyn
- name: Rek'Sai
- name: Jax
other:
- cost: 1
copies_taken: 2
- name: Ahri
copies_taken: 5
9 changes: 0 additions & 9 deletions examples/lvl4_1costs.json

This file was deleted.

9 changes: 0 additions & 9 deletions examples/lvl7_heartsteel.json

This file was deleted.

16 changes: 0 additions & 16 deletions examples/lvl7_heartsteel_copiestaken.json

This file was deleted.

10 changes: 0 additions & 10 deletions examples/lvl8_executioner.json

This file was deleted.

9 changes: 0 additions & 9 deletions examples/lvl8_heartsteel.json

This file was deleted.

11 changes: 0 additions & 11 deletions examples/sentinel_ahri.json

This file was deleted.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ dynamic = ["version"]

[project.scripts]
rolldown = "roll_sim.scripts.rolldown:cli"
headliners = "roll_sim.scripts.headliner_rolldown:cli"
roll_level = "roll_sim.scripts.rollorlvl:cli"

[tool.ruff]
Expand Down
26 changes: 14 additions & 12 deletions roll_sim/code/champion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from dataclasses import KW_ONLY, dataclass
from functools import lru_cache


@dataclass
Expand All @@ -13,30 +14,31 @@ class Champion:
odds: int | float | None = None

traits: list[str] | None = None
headlined_trait: str | None = None
headliner_odds: int | float | None = None

pool_size: int = 0
copies_taken: int = 0
copies_held: int = 0
copies_necessary: int = 9

@property
def copies_left(self) -> int:
return self.pool_size - self.copies_taken
# @property
# def copies_left(self) -> int:
# return self.pool_size - self.copies_taken

def __eq__(self, other) -> bool:
"""Define equality as champions having same name and same headlined trait."""
if isinstance(other, Champion):
name_match = (self.name == other.name)
if self.headlined_trait is not None and other.headlined_trait is not None:
headlined_trait_match = (self.headlined_trait == other.headlined_trait)
return name_match and headlined_trait_match

return name_match
return self.name == other.name
return False

def __hash__(self) -> int:
"""Hash the Champion instance based on its name."""
return hash(self.name)

@staticmethod
def from_list(champions_data: list[dict]) -> list[Champion]:
"""Create list of champion objects from list of dictionares."""
return [Champion(**champion_data) for champion_data in champions_data]
return [Champion(**champion_data) for champion_data in champions_data]

@lru_cache(maxsize = 1024)
def odds_cache(odds, pool_size, copies_taken):
return odds*(pool_size - copies_taken)/pool_size
173 changes: 62 additions & 111 deletions roll_sim/code/roll_champions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,124 +3,85 @@
import random
from copy import deepcopy

from ..code.champion import Champion
from ..code.champion import Champion, odds_cache


class RolldownSimulator:
def __init__(self, champions: list[Champion], champions_to_buy: list[Champion]):
self.champions = champions

self.cost_keys = list(self.champions.keys())
self.cost_odds = [ data["odds"] for data in self.champions.values() ]

self.odds = {}
for cost in self.champions:
self.odds[cost] = []
for champ in self.champions[cost]["champions"]:
self.odds[cost].append(odds_cache(champ.odds, champ.pool_size, champ.copies_taken))

self.champions_to_buy_indexes = []
self.champions_to_buy = champions_to_buy
for cost in self.champions:
for champ_to_buy in self.champions_to_buy:
for champ in self.champions[cost]["champions"]:
if champ_to_buy == champ:
self.champions_to_buy_indexes.append((cost, self.champions[cost]["champions"].index(champ) ))

self.odds_initial = deepcopy(self.odds)
self.champions_initial = deepcopy(self.champions)
self.champions_needed = champions_to_buy
self.champions_to_buy = champions_to_buy
self.rolls_list = []

self.headliner_bought = False
# Check whether they aren't any incorrect champions.
# if not (all((incorrect := champ) in self.champions for champ in self.champions_to_buy)):
# raise ValueError(f"Incorrect '{incorrect.name}' champion in config.")

self.rolls_list = []
self.headliner_rolls = []
self.last_seven_shops = []

@property
def champions_to_buy(self):
return [champ for champ_to_buy in self.champions_needed
for champ in self.champions
if champ_to_buy.name == champ.name]

@property
def odds(self):
return [champion.odds*(champion.copies_left/champion.pool_size)
for champion in self.champions]

@property
def headliner_odds(self):
return [champion.headliner_odds*(champion.copies_left/champion.pool_size)
for champion in self.champions]

def valid_headliner_shop(self, random_headliner: Champion, headlined_trait: str) -> bool:
"""Check if rolled headliner doesn't break bad luck protection rules."""
# A champion with same headlined_trait cannot appear again with the same headlined trait.
if random_headliner.headlined_trait == headlined_trait:
return False

# 1, 2 and 3 cost headliners cannot appear again for 7 shops.
if random_headliner.cost in [1, 2, 3] and random_headliner in self.last_seven_shops:
return False

# 4 costs headliner cannot appear again for 5 shops.
if random_headliner.cost == 4:
if random_headliner.copies_held > 4:
return False
if random_headliner in self.last_seven_shops[-5:]:
return False

# 5 costs headliner cannot appear again for 4 shops.
if random_headliner.cost == 5:
if random_headliner.copies_held > 3:
return False
if random_headliner in self.last_seven_shops[-4:]:
return False

# Same headlined trait cannot appear for 4 shops.
if headlined_trait in [champion.headlined_trait for champion in self.last_seven_shops[-4:]]:
return False

return True

def headliner_shop(self, bad_luck_rules: bool) -> None:
while True:
headliner = random.choices(self.champions, weights=self.headliner_odds)[0]
headlined_trait = random.choices(headliner.traits)[0]

# Check if bad luck rules apply.
if bad_luck_rules and not self.valid_headliner_shop(headliner, headlined_trait):
continue

#Check if enough copies left.
if headliner.copies_left < 3:
continue

# If conditions are met, break out of the loop
break

headliner.headlined_trait = headlined_trait
self.last_seven_shops = self.last_seven_shops[-6:]
self.last_seven_shops.append(headliner)

if not self.headliner_bought and headliner in self.champions_needed:
self.headliner_bought = True
headliner.copies_held += 3

def shop(self, headliner_shop: bool, bad_luck_rules: bool) -> None:
# Headliner shop is first.
if headliner_shop:
self.headliner_shop(bad_luck_rules)

champs = []
normal_shops = 5 if not headliner_shop else 4
# Normal shop logic
for shop in range(normal_shops):
champs.append(champ := random.choices(self.champions, weights=self.odds)[0])

# self.odds = [odds_cache(champ.odds, champ.pool_size, champ.copies_taken) for champ in self.champions]

# @property
# def odds(self):
# return [champion.odds*(champion.copies_left/champion.pool_size)
# for champion in self.champions]

# @property
# def odds(self):
# return [odds_cache(champ.odds, champ.pool_size, champ.copies_taken) for champ in self.champions]

def shop(self, num_shops=5) -> None:

indexes = []
for shop in range(num_shops):
cost_key = random.choices(self.cost_keys, self.cost_odds)[0]
cost_champions = self.champions[cost_key]["champions"]

index = random.choices(list(range(len(cost_champions))), weights=self.odds[cost_key])[0]

indexes.append((cost_key, index))

champ = self.champions[cost_key]["champions"][index]
champ.copies_taken += 1

for champ in champs:
if champ in self.champions_to_buy:
self.odds[cost_key][index] = odds_cache(champ.odds, champ.pool_size, champ.copies_taken)

for cost_key, index in indexes:
champ = self.champions[cost_key]["champions"][index]
if champ in self.champions_to_buy and (champ.copies_necessary > champ.copies_held):
champ.copies_held += 1
else:
champ.copies_taken -= 1

def roll(self, rolldowns: int = 1000, headliner_mechanic: bool = True, bad_luck_rules: bool = True) -> float:
"""Roll for given headliners with a given headlined Trait.
self.odds[cost_key][index] = odds_cache(champ.odds, champ.pool_size, champ.copies_taken)

def roll(self, rolldowns: int = 1000, num_shops = 5) -> float:
"""Roll for given champions.

Args:
----
rolldowns (int, optional): Number of independent rolldowns.
The bigger the number, the more accurate the results.
Defaults to 10000.

headliner_mechanic (bool, optional): Whether to apply headliner logic.
Defaults to True.

bad_luck_rules (bool, optional): Whether to apply bad luck protection rules.
Defaults to True.

Returns:
-------
Float: Average rolls needed to hit all of your requested champions.
Expand All @@ -130,26 +91,16 @@ def roll(self, rolldowns: int = 1000, headliner_mechanic: bool = True, bad_luck_
# Reset for every rolldown
rolls = 0
self.champions = deepcopy(self.champions_initial)
self.last_seven_shops = []
self.headliner_bought = False
shops_counter = 0
self.odds = deepcopy(self.odds_initial)
self.champions_needed = [self.champions[cost_key]["champions"][index] for cost_key, index in self.champions_to_buy_indexes]

# Rolls condition ensures script finishes.
while rolls < 1000:

if headliner_mechanic:
if not self.headliner_bought or shops_counter % 4 == 0:
self.shop(headliner_shop=True, bad_luck_rules=bad_luck_rules)
else:
self.shop(headliner_shop=False)
shops_counter = 0 if shops_counter % 4 == 0 else shops_counter + 1
else:
self.shop(headliner_shop=False)

self.shop(num_shops=num_shops)
rolls += 1

# If found what requested, stop current rolldown.
if all(champ.copies_held >= champ.copies_necessary for champ in self.champions_to_buy):
if all(champ.copies_held >= champ.copies_necessary for champ in self.champions_needed):
self.rolls_list.append(rolls)
break

Expand Down
Loading
Loading