Files
Archipelago/worlds/yachtdice/Rules.py
2024-06-07 21:49:24 +02:00

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)