mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-03 07:23:41 -07:00
203 lines
8.1 KiB
Python
203 lines
8.1 KiB
Python
from ..generic.Rules import set_rule
|
|
from BaseClasses import MultiWorld
|
|
from .YachtWeights import yacht_weights
|
|
import math
|
|
|
|
#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):
|
|
self.name = name
|
|
|
|
#return mean score of a category
|
|
def meanScore(self, nbDice, nbRolls):
|
|
if nbDice == 0 or nbRolls == 0:
|
|
return 0
|
|
meanScore = 0
|
|
for key in yacht_weights[self.name, min(8,nbDice), min(8,nbRolls)]:
|
|
meanScore += key*yacht_weights[self.name, min(8,nbDice), min(8,nbRolls)][key]/100000
|
|
return meanScore
|
|
|
|
|
|
|
|
def extractProgression(state, player, options):
|
|
#method to obtain a list of what items the player has.
|
|
#this includes categories, dice, rolls and score multiplier.
|
|
|
|
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
|
|
)
|
|
|
|
number_of_mults = state.count("Score Multiplier", player)
|
|
|
|
|
|
score_mult = -10000
|
|
if options.score_multiplier_type.value == 1: #fixed
|
|
score_mult = 0.1 * number_of_mults
|
|
if options.score_multiplier_type.value == 2: #step
|
|
score_mult = 0.01 * number_of_mults
|
|
|
|
categories = []
|
|
|
|
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": "TwoTimesOnes",
|
|
"Category Half of Sixes": "HalfOfSixes",
|
|
"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"
|
|
}
|
|
|
|
for category_name, category_value in category_mappings.items():
|
|
if state.has(category_name, player, 1):
|
|
categories.append(Category(category_value))
|
|
|
|
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, score_mult, 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 diceSimulationStrings(categories, nbDice, nbRolls, multiplier, diff, scoremulttype):
|
|
tup = tuple([tuple(sorted([c.name for c in categories])), nbDice, nbRolls, multiplier]) #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-scorig categories first
|
|
categories.sort(key=lambda category: category.meanScore(nbDice, nbRolls))
|
|
|
|
#function to add two discrete distribution.
|
|
def add_distributions(dist1, dist2, mult):
|
|
combined_dist = {}
|
|
for val1, prob1 in dist1.items():
|
|
for val2, prob2 in dist2.items():
|
|
if int(val1 + val2 * mult) in combined_dist.keys():
|
|
combined_dist[int(val1 + val2 * mult)] += prob1 * prob2
|
|
else:
|
|
combined_dist[int(val1 + val2 * mult)] = prob1 * prob2
|
|
return combined_dist
|
|
|
|
#function to take the maximum of 'times' i.i.d. dist1.
|
|
def max_dist(dist1, times):
|
|
new_dist = {0: 1}
|
|
for _ in range(times):
|
|
c = new_dist.copy()
|
|
new_dist = {}
|
|
for val1, prob1 in c.items():
|
|
for val2, prob2 in dist1.items():
|
|
new_val = max(val1, val2)
|
|
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]
|
|
|
|
#calculate total distribution
|
|
total_dist = {0: 1}
|
|
for j in range(len(categories)):
|
|
if nbDice == 0 or nbRolls == 0:
|
|
dist = {0: 100000}
|
|
else:
|
|
dist = yacht_weights[categories[j].name, min(8,nbDice), min(8,nbRolls)].copy()
|
|
|
|
for key in dist.keys():
|
|
dist[key] /= 100000
|
|
|
|
#for higher difficulties, the simulation gets multiple tries for categories.
|
|
dist = max_dist(dist, max(1, len(categories) // (6 - diff)))
|
|
|
|
cur_mult = -100
|
|
if scoremulttype == 1: #fixed
|
|
cur_mult = multiplier
|
|
if scoremulttype == 2: #step
|
|
cur_mult = j * multiplier
|
|
total_dist = add_distributions(total_dist, dist, 1 + cur_mult )
|
|
|
|
#save result into the cache, then return it
|
|
yachtdice_cache[tup] = math.floor(percentile_distribution(total_dist, .40))
|
|
return yachtdice_cache[tup]
|
|
|
|
# Returns the feasible score that one can reach with the current state, options and difficulty.
|
|
def diceSimulation(state, player, options):
|
|
categories, nbDice, nbRolls, multiplier, expoints = extractProgression(state, player, options)
|
|
return diceSimulationStrings(categories, nbDice, nbRolls, multiplier,
|
|
options.game_difficulty.value, options.score_multiplier_type.value) + expoints
|
|
|
|
|
|
|
|
# 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:
|
|
diceSimulation(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) |