mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 20:03:21 -07:00
235 lines
10 KiB
Python
235 lines
10 KiB
Python
from ..generic.Rules import set_rule
|
|
from BaseClasses import MultiWorld
|
|
from .YachtWeights import yacht_weights
|
|
import math
|
|
from collections import defaultdict
|
|
|
|
#List of categories, and the name of the logic class associated with it
|
|
category_mappings = {
|
|
"Category Ones": "Ones",
|
|
"Category Twos": "Twos",
|
|
"Category Threes": "Threes",
|
|
"Category Fours": "Fours",
|
|
"Category Fives": "Fives",
|
|
"Category Sixes": "Sixes",
|
|
"Category Choice": "Choice",
|
|
"Category Inverse Choice": "Choice",
|
|
"Category Pair": "Pair",
|
|
"Category Three of a Kind": "ThreeOfAKind",
|
|
"Category Four of a Kind": "FourOfAKind",
|
|
"Category Tiny Straight": "TinyStraight",
|
|
"Category Small Straight": "SmallStraight",
|
|
"Category Large Straight": "LargeStraight",
|
|
"Category Full House": "FullHouse",
|
|
"Category Yacht": "Yacht",
|
|
|
|
"Category Distincts": "Distincts",
|
|
"Category Two times Ones": "Twos", #same weights as twos category
|
|
"Category Half of Sixes": "Threes", #same weights as threes category
|
|
"Category Twos and Threes": "TwosAndThrees",
|
|
"Category Sum of Odds": "SumOfOdds",
|
|
"Category Sum of Evens": "SumOfEvens",
|
|
"Category Double Threes and Fours": "DoubleThreesAndFours",
|
|
"Category Quadruple Ones and Twos": "QuadrupleOnesAndTwos",
|
|
"Category Micro Straight": "MicroStraight",
|
|
"Category Three Odds": "ThreeOdds",
|
|
"Category 1-2-1 Consecutive": "OneTwoOneConsecutive",
|
|
"Category Three Distinct Dice": "ThreeDistinctDice",
|
|
"Category Two Pair": "TwoPair",
|
|
"Category 2-1-2 Consecutive": "TwoOneTwoConsecutive",
|
|
"Category Five Distinct Dice": "FiveDistinctDice",
|
|
"Category 4&5 Full House": "FourAndFiveFullHouse"
|
|
}
|
|
|
|
#This class adds logic to the apworld.
|
|
#In short, we ran a simulation for every possible combination of dice and rolls you can have, per category.
|
|
#This simulation has a good strategy for locking dice.
|
|
#This gives rise to an approximate discrete distribution per category.
|
|
#We calculate the distribution of the total score.
|
|
#We then pick a correct percentile to reflect the correct score that should be in logic.
|
|
#The score is logic is *much* lower than the actual maximum reachable score.
|
|
|
|
|
|
class Category:
|
|
def __init__(self, name, quantity = 1):
|
|
self.name = name
|
|
self.quantity = quantity #how many times you have the category
|
|
|
|
#return mean score of a category
|
|
def mean_score(self, num_dice, num_rolls):
|
|
if num_dice == 0 or num_rolls == 0:
|
|
return 0
|
|
mean_score = 0
|
|
for key in yacht_weights[self.name, min(8,num_dice), min(8,num_rolls)]:
|
|
mean_score += key*yacht_weights[self.name, min(8,num_dice), min(8,num_rolls)][key]/100000
|
|
return mean_score * self.quantity
|
|
|
|
|
|
def extract_progression(state, player, options):
|
|
#method to obtain a list of what items the player has.
|
|
#this includes categories, dice, rolls and score multiplier etc.
|
|
|
|
if player == "state_is_a_list": #the state variable is just a list with the names of the items
|
|
number_of_dice = (
|
|
state.count("Dice")
|
|
+ state.count("Dice Fragment") // options.number_of_dice_fragments_per_dice.value
|
|
)
|
|
number_of_rerolls = (
|
|
state.count("Roll")
|
|
+ state.count("Roll Fragment") // options.number_of_roll_fragments_per_roll.value
|
|
)
|
|
roll = state.count("Roll")
|
|
rollfragments = state.count("Roll Fragment")
|
|
number_of_fixed_mults = state.count("Fixed Score Multiplier")
|
|
number_of_step_mults = state.count("Step Score Multiplier")
|
|
categories = []
|
|
for category_name, category_value in category_mappings.items():
|
|
if state.count(category_name) >= 1:
|
|
categories += [Category(category_value, state.count(category_name))]
|
|
extra_points_in_logic = state.count("1 Point")
|
|
extra_points_in_logic += state.count("10 Points") * 10
|
|
extra_points_in_logic += state.count("100 Points") * 100
|
|
else: #state is an Archipelago object, so we need state.count(..., player)
|
|
number_of_dice = (
|
|
state.count("Dice", player)
|
|
+ state.count("Dice Fragment", player) // options.number_of_dice_fragments_per_dice.value
|
|
)
|
|
number_of_rerolls = (
|
|
state.count("Roll", player)
|
|
+ state.count("Roll Fragment", player) // options.number_of_roll_fragments_per_roll.value
|
|
)
|
|
roll = state.count("Roll", player)
|
|
rollfragments = state.count("Roll Fragment", player)
|
|
number_of_fixed_mults = state.count("Fixed Score Multiplier", player)
|
|
number_of_step_mults = state.count("Step Score Multiplier", player)
|
|
categories = []
|
|
for category_name, category_value in category_mappings.items():
|
|
if state.count(category_name, player) >= 1:
|
|
categories += [Category(category_value, state.count(category_name, player))]
|
|
extra_points_in_logic = state.count("1 Point", player)
|
|
extra_points_in_logic += state.count("10 Points", player) * 10
|
|
extra_points_in_logic += state.count("100 Points", player) * 100
|
|
|
|
return [categories, number_of_dice, number_of_rerolls,
|
|
number_of_fixed_mults * 0.1, number_of_step_mults * 0.01, extra_points_in_logic]
|
|
|
|
#We will store the results of this function as it is called often for the same parameters.
|
|
yachtdice_cache = {}
|
|
|
|
#Function that returns the feasible score in logic based on items obtained.
|
|
def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff):
|
|
tup = tuple([tuple(sorted([c.name+str(c.quantity) for c in categories])),
|
|
num_dice, num_rolls, fixed_mult, step_mult, diff]) #identifier
|
|
|
|
#if already computed, return the result
|
|
if tup in yachtdice_cache.keys():
|
|
return yachtdice_cache[tup]
|
|
|
|
#sort categories because for the step multiplier, you will want low-scoring categories first
|
|
categories.sort(key=lambda category: category.mean_score(num_dice, num_rolls))
|
|
|
|
#function to add two discrete distribution.
|
|
#defaultdict is a dict where you don't need to check if a id is present, you can just use += (lot faster)
|
|
def add_distributions(dist1, dist2):
|
|
combined_dist = defaultdict(float)
|
|
for val1, prob1 in dist1.items():
|
|
for val2, prob2 in dist2.items():
|
|
combined_dist[val1 + val2] += prob1 * prob2
|
|
return dict(combined_dist)
|
|
|
|
#function to take the maximum of "times" i.i.d. dist1.
|
|
#(I have tried using defaultdict here too but this made it slower.)
|
|
def max_dist(dist1, mults):
|
|
new_dist = {0: 1}
|
|
for mult in mults:
|
|
c = new_dist.copy()
|
|
new_dist = {}
|
|
for val1, prob1 in c.items():
|
|
for val2, prob2 in dist1.items():
|
|
new_val = int(max(val1, val2 * mult))
|
|
new_prob = prob1 * prob2
|
|
|
|
# Update the probability for the new value
|
|
if new_val in new_dist:
|
|
new_dist[new_val] += new_prob
|
|
else:
|
|
new_dist[new_val] = new_prob
|
|
|
|
return new_dist
|
|
|
|
#Returns percentile value of a distribution.
|
|
def percentile_distribution(dist, percentile):
|
|
sorted_values = sorted(dist.keys())
|
|
cumulative_prob = 0
|
|
prev_val = None
|
|
|
|
for val in sorted_values:
|
|
prev_val = val
|
|
cumulative_prob += dist[val]
|
|
if cumulative_prob >= percentile:
|
|
return prev_val # Return the value before reaching the desired percentile
|
|
|
|
# Return the first value if percentile is lower than all probabilities
|
|
return prev_val if prev_val is not None else sorted_values[0]
|
|
|
|
#parameters for logic.
|
|
#perc_return is, per difficulty, the percentages of total score it returns (it averages out the values)
|
|
#diff_divide determines how many shots the logic gets per category. Lower = more shots.
|
|
perc_return = [[0], [0.1, 0.5], [0.3, 0.7], [0.55, 0.85], [0.85, 0.95]][diff]
|
|
diff_divide = [0, 9, 7, 3, 2][diff]
|
|
|
|
#calculate total distribution
|
|
total_dist = {0: 1}
|
|
for j in range(len(categories)):
|
|
if num_dice == 0 or num_rolls == 0:
|
|
dist = {0: 100000}
|
|
else:
|
|
dist = yacht_weights[categories[j].name, min(8,num_dice), min(8,num_rolls)].copy()
|
|
|
|
for key in dist.keys():
|
|
dist[key] /= 100000
|
|
|
|
cat_mult = 2 ** (categories[j].quantity-1)
|
|
|
|
#for higher difficulties, the simulation gets multiple tries for categories.
|
|
max_tries = j // diff_divide
|
|
mults = [(1 + fixed_mult + step_mult * ii) * cat_mult for ii in range(max(0,j - max_tries), j+1)]
|
|
dist = max_dist(dist, mults)
|
|
|
|
total_dist = add_distributions(total_dist, dist)
|
|
|
|
#save result into the cache, then return it
|
|
outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return)
|
|
yachtdice_cache[tup] = max(5, math.floor(outcome))
|
|
return yachtdice_cache[tup]
|
|
|
|
# Returns the feasible score that one can reach with the current state, options and difficulty.
|
|
def dice_simulation(state, player, options):
|
|
if player == "state_is_a_list":
|
|
categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression(state, player, options)
|
|
return dice_simulation_strings(
|
|
categories, num_dice, num_rolls, fixed_mult, step_mult, options.game_difficulty.value
|
|
) + expoints
|
|
|
|
if state.prog_items[player]["state_is_fresh"] == 0:
|
|
state.prog_items[player]["state_is_fresh"] = 1
|
|
categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression(state, player, options)
|
|
state.prog_items[player]["maximum_achievable_score"] = dice_simulation_strings(
|
|
categories, num_dice, num_rolls, fixed_mult, step_mult, options.game_difficulty.value
|
|
) + expoints
|
|
|
|
return state.prog_items[player]["maximum_achievable_score"]
|
|
|
|
|
|
# Sets rules on entrances and advancements that are always applied
|
|
def set_yacht_rules(world: MultiWorld, player: int, options):
|
|
for l in world.get_locations(player):
|
|
set_rule(l,
|
|
lambda state,
|
|
curscore=l.yacht_dice_score,
|
|
player=player:
|
|
dice_simulation(state, player, options) >= curscore)
|
|
|
|
# Sets rules on completion condition
|
|
def set_yacht_completion_rules(world: MultiWorld, player: int):
|
|
world.completion_condition[player] = lambda state: state.has("Victory", player) |